From 3313c52086910518e134eb8a054268723f530f4d Mon Sep 17 00:00:00 2001 From: Stepan Usatiuk Date: Thu, 15 Oct 2020 14:53:41 +0300 Subject: [PATCH] sort pictures by most recent --- frontend/src/Photos/Overview.tsx | 6 ++--- frontend/src/Photos/PhotoCard.tsx | 4 ++-- package-lock.json | 14 ++++++++++++ package.json | 1 + src/entity/Photo.ts | 32 +++++++++++++++++++++++---- src/routes/photos.ts | 9 ++++---- src/tests/integration/photos.test.ts | 30 ++++++++++++++++--------- src/tests/integration/photos/cat.jpg | Bin 58332 -> 65074 bytes src/tests/integration/photos/dog.jpg | Bin 67292 -> 74034 bytes src/util.ts | 15 ++++++++++++- 10 files changed, 86 insertions(+), 25 deletions(-) diff --git a/frontend/src/Photos/Overview.tsx b/frontend/src/Photos/Overview.tsx index 78b649b..72068d5 100644 --- a/frontend/src/Photos/Overview.tsx +++ b/frontend/src/Photos/Overview.tsx @@ -29,9 +29,9 @@ export const OverviewComponent: React.FunctionComponent return ; } - const photos = props.photos.map((photo) => ( - - )); + const photos = props.photos + .sort((a, b) => b.shotAt - a.shotAt) + .map((photo) => ); return (
diff --git a/frontend/src/Photos/PhotoCard.tsx b/frontend/src/Photos/PhotoCard.tsx index b1faa2e..c076188 100644 --- a/frontend/src/Photos/PhotoCard.tsx +++ b/frontend/src/Photos/PhotoCard.tsx @@ -42,7 +42,7 @@ export class PhotoCardComponent extends React.PureComponent< } */ public render(): JSX.Element { - const isUploaded = this.props.photo.uploaded; + const fileExists = this.props.photo.uploaded; return ( - {isUploaded ? ( + {fileExists ? ( ) : ( diff --git a/package-lock.json b/package-lock.json index 7814a12..87080dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2177,6 +2177,14 @@ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" }, + "exifreader": { + "version": "3.12.3", + "resolved": "https://registry.npmjs.org/exifreader/-/exifreader-3.12.3.tgz", + "integrity": "sha512-8CtkjDC8xcREleBoFE1xZzkbaEiuLngORo6Xbxn3rPl/NTznTFHK0F9zg5bz5nwWmGDfPArPUXpUyJeHQHy+fA==", + "requires": { + "xmldom": "^0.1.31" + } + }, "expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -5648,6 +5656,12 @@ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" }, + "xmldom": { + "version": "0.1.31", + "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.31.tgz", + "integrity": "sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ==", + "optional": true + }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index a12ab01..c38ca60 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "eslint-plugin-import": "^2.22.1", "eslint-plugin-mocha": "^8.0.0", "eslint-plugin-prettier": "^3.1.4", + "exifreader": "^3.12.3", "hasha": "^5.2.2", "husky": "^4.3.0", "jsonwebtoken": "^8.5.1", diff --git a/src/entity/Photo.ts b/src/entity/Photo.ts index 92b5d35..9cae670 100644 --- a/src/entity/Photo.ts +++ b/src/entity/Photo.ts @@ -29,7 +29,7 @@ import { validateOrReject, } from "class-validator"; import { config } from "~config"; -import { resizeTo } from "~util"; +import { getShotDate, resizeTo } from "~util"; export interface IPhotoJSON { id: number; @@ -69,6 +69,9 @@ export class Photo extends BaseEntity { @IsMimeType() public format: string; + @Column({ default: false }) + public uploaded: boolean; + @Column({ type: "set", enum: ThumbSizes, default: [] }) public generatedThumbs: string[]; @@ -137,17 +140,38 @@ export class Photo extends BaseEntity { } } - public async isUploaded(): Promise { + // Checks if file exists and updates the DB + public async fileExists(): Promise { try { await fs.access(this.getPath(), fsConstants.F_OK); + if (!this.uploaded) { + this.uploaded = true; + await this.save(); + } return true; } catch (e) { + if (this.uploaded) { + this.uploaded = false; + this.generatedThumbs = []; + await this.save(); + } return false; } } + public async processUpload(): Promise { + await this.fileExists(); + const date = await getShotDate(this.getPath()); + if (date !== null) { + this.shotAt = date; + } else { + this.shotAt = new Date(); + } + await this.save(); + } + private async generateThumbnail(size: number): Promise { - if (!(await this.isUploaded())) { + if (!(await this.fileExists())) { return; } await resizeTo(this.getPath(), this.getThumbPath(size), size); @@ -211,7 +235,7 @@ export class Photo extends BaseEntity { createdAt: this.createdAt.getTime(), editedAt: this.editedAt.getTime(), shotAt: this.shotAt.getTime(), - uploaded: await this.isUploaded(), + uploaded: this.uploaded, }; } diff --git a/src/routes/photos.ts b/src/routes/photos.ts index 2338af3..0fcdab0 100644 --- a/src/routes/photos.ts +++ b/src/routes/photos.ts @@ -84,7 +84,7 @@ photosRouter.post("/photos/upload/:id", async (ctx) => { return; } - if (await photo.isUploaded()) { + if (await photo.fileExists()) { ctx.throw(400, "Already uploaded"); return; } @@ -108,6 +108,7 @@ photosRouter.post("/photos/upload/:id", async (ctx) => { try { // TODO: actually move file if it's on different filesystems await fs.rename(file.path, photo.getPath()); + await photo.processUpload(); } catch (e) { console.log(e); ctx.throw(500); @@ -236,7 +237,7 @@ photosRouter.get("/photos/showByID/:id/:token", async (ctx) => { user: { id: user }, }); - if (!photo || !(await photo.isUploaded())) { + if (!photo || !(await photo.fileExists())) { ctx.throw(404); return; } @@ -267,7 +268,7 @@ photosRouter.get("/photos/showByID/:id", async (ctx) => { const photo = await Photo.findOne({ id, user }); - if (!photo || !(await photo.isUploaded())) { + if (!photo || !(await photo.fileExists())) { ctx.throw(404); return; } @@ -299,7 +300,7 @@ photosRouter.get("/photos/getShowByIDToken/:id", async (ctx) => { const { user } = ctx.state; const photo = await Photo.findOne({ id, user }); - if (!photo || !(await photo.isUploaded())) { + if (!photo || !(await photo.fileExists())) { ctx.throw(404); return; } diff --git a/src/tests/integration/photos.test.ts b/src/tests/integration/photos.test.ts index 19eccc6..a89f973 100644 --- a/src/tests/integration/photos.test.ts +++ b/src/tests/integration/photos.test.ts @@ -178,7 +178,7 @@ describe("photos", function () { expect(response.body.error).to.be.equal("Not Found"); }); - it("should create, upload and show a photo", async function () { + it("should create, upload and show a photo with a shot date", async function () { const response = await request(callback) .post("/photos/new") .set({ @@ -203,7 +203,7 @@ describe("photos", function () { }); expect(dbPhoto.hash).to.be.equal(dogHash); - expect(await dbPhoto.isUploaded()).to.be.equal(false); + expect(await dbPhoto.fileExists()).to.be.equal(false); await request(callback) .post(`/photos/upload/${photo.id}`) @@ -214,7 +214,15 @@ describe("photos", function () { .attach("photo", dogPath) .expect(200); - expect(await dbPhoto.isUploaded()).to.be.equal(true); + const dbPhotoUpl = await Photo.findOneOrFail({ + id: photo.id, + user: seed.user1.id as any, + }); + expect(dbPhotoUpl.hash).to.be.equal(dogHash); + expect(await dbPhotoUpl.fileExists()).to.be.equal(true); + expect(dbPhotoUpl.shotAt.toISOString()).to.be.equal( + new Date("2020-10-05T14:20:18").toISOString(), + ); const showResp = await request(callback) .get(`/photos/showByID/${photo.id}`) @@ -290,7 +298,7 @@ describe("photos", function () { }); expect(dbPhoto.hash).to.be.equal(dogHash); - expect(await dbPhoto.isUploaded()).to.be.equal(false); + expect(await dbPhoto.fileExists()).to.be.equal(false); await request(callback) .post(`/photos/upload/${photo.id}`) @@ -301,7 +309,7 @@ describe("photos", function () { .attach("photo", dogPath) .expect(200); - expect(await dbPhoto.isUploaded()).to.be.equal(true); + expect(await dbPhoto.fileExists()).to.be.equal(true); await request(callback) .post(`/photos/upload/${photo.id}`) @@ -349,7 +357,7 @@ describe("photos", function () { }); expect(dbPhoto.hash).to.be.equal(dogHash); - expect(await dbPhoto.isUploaded()).to.be.equal(false); + expect(await dbPhoto.fileExists()).to.be.equal(false); await request(callback) .post(`/photos/upload/${photo.id}`) @@ -360,7 +368,7 @@ describe("photos", function () { .attach("photo", catPath) .expect(400); - expect(await dbPhoto.isUploaded()).to.be.equal(false); + expect(await dbPhoto.fileExists()).to.be.equal(false); const showResp = await request(callback) .get(`/photos/showByID/${photo.id}`) @@ -394,7 +402,7 @@ describe("photos", function () { user: seed.user1.id as any, }); expect(dbPhoto.hash).to.be.equal(dogHash); - expect(await dbPhoto.isUploaded()).to.be.equal(false); + expect(await dbPhoto.fileExists()).to.be.equal(false); await request(callback) .post(`/photos/upload/${photo.id}`) @@ -405,7 +413,7 @@ describe("photos", function () { .attach("photo", dogPath) .expect(404); - expect(await dbPhoto.isUploaded()).to.be.equal(false); + expect(await dbPhoto.fileExists()).to.be.equal(false); }); it("should create, upload but not show a photo to another user", async function () { @@ -432,7 +440,7 @@ describe("photos", function () { user: seed.user1.id as any, }); expect(dbPhoto.hash).to.be.equal(dogHash); - expect(await dbPhoto.isUploaded()).to.be.equal(false); + expect(await dbPhoto.fileExists()).to.be.equal(false); await request(callback) .post(`/photos/upload/${photo.id}`) @@ -443,7 +451,7 @@ describe("photos", function () { .attach("photo", dogPath) .expect(200); - expect(await dbPhoto.isUploaded()).to.be.equal(true); + expect(await dbPhoto.fileExists()).to.be.equal(true); await request(callback) .get(`/photos/showByID/${photo.id}`) diff --git a/src/tests/integration/photos/cat.jpg b/src/tests/integration/photos/cat.jpg index fe7cb57cd796c734167cd107bac5660b432b14df..fda3bce7d734fb8d8a11a9420d0eedc98f115e6e 100644 GIT binary patch delta 2679 zcmciEKX21O6aes}DKsFEDg;{xE0G97vd?y6H96Vo&`?;yP%1H>eNJn&{v+E_OqQ^9 zsuUt-#8;pl8DL{(VPfgpg)hLsxlMDi6+@^SEIWSh^7MY^H{Fk~h2vj^;X~zg_4sLI zbpU9$!4m*L5iA{AV3|fYXd<7jfff3`OY=wc0c&UUr+D)1`%3ZI&0zs7(ewcTt8a$O zlla}}?Ri`f>i~>KEN0D$VWQcs;$vBqMI^;HC)E&*#dR=i?c@EU^5FFLhrKZL5QI)3 zVB+egV7NAnum>eUgh1VhJZ$WnA*Y+WmZMdEe)?SDETUCjDQ&UsZJK-5^S*Ds?7!GC z`uj$MR5qIT?x+!pY|l1B%tiKr6QD>dop(nxj+bGnsrsZdK-;aYaS2Ve(s(nyUQg&% z1=rt&Qlrs;q6}r3rxkqg+6i&QJ3;xP18SLp;ags4xeiAQ5Z2vpsFk{1i=YnfC^{hu zuj4A=C5d1jld8m%YL%=jQe7T&B)xRGMcbSBGlxMz`cY`pH*sjT=+lhH%1iORb2(~3 ztk+bSs#9USv#FgnFIKh(tt}c(hD9gG{fCPog9U0#$wu<~^C1fJdpu*HNGUcxzy7GO1DNU1Q&*23uTI{Vn_qqdc-*yF delta 18 acmdn=hxyKN<_V&kPYQG?Z~kg)djJ4gfe9D@ diff --git a/src/tests/integration/photos/dog.jpg b/src/tests/integration/photos/dog.jpg index ac32d2fa4e6bf8ce50e4f3099fe17ae9c1b4f62c..f04ef5c1072e6f0f87bdf22bf24ae11c184d8d0c 100644 GIT binary patch delta 2679 zcmciEKX21O6aes}4b&izDg;{x%aI5|vd@3&(wuB}XecaUD3zGcKD)I_94m1YH;cP5 zRSFR^f{#EuG9d9eSSq$Id;k{CZJJB01Wer!$;o>!Pw#hr(|tcG9e*qJAL^&e$IoKl z1E5|9PXGWMSa|D$5{+)qL_J#pOZ4|H%^%SRtenv=?&Q<2CGPpn{vueQ=>q_k5BsH2 z{C@E6JYLk+02mBd%!YB%#IRf3V^vmVq{v9td1V!)@j94R`+WbXoSfeNxEDnsf-q=H zm^5uya+(bo;}9y641uu`huGP7Bi?p*{lKjJ{PMNJ`^2og*6VUT+;sQ+7rmDIs`qlo z>FqmJQrWQX-7#VmH^PP+VLooW3EC(&h4bl%#%UP}meC?!g0^d0!xoyF!tgNNZddAR zQnR%Sm1?yLWfiKbNIS&#K@j0s4BF+338?0_ot7U)ely@{1HyK*6PZG%;}hgzPqzut zMH_2GR1|_mOf*F#nns@L%DS3(iY;6ou@R2^smq`seJQlra&hF==wqd86%|>O^_|OA zOWK-dz)YV3)02&z)Vio_C$%jaj+R9?$Nz^9Ly|daOqq=o_wy*l{bEX9e_qOjKYppQFOmEHdV^rovHHnLV(GpI<5BUx)H{f4<7l wpMn(G|BSbqqRi-|g^m=vjp=|y54X=Q;2mk;8 delta 18 acmdmVh~-W%%LLKQCk48cH-ELYeGC9ny9mhu diff --git a/src/util.ts b/src/util.ts index a92cec6..a9a7b8a 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,6 +1,8 @@ import deasync = require("deasync"); import { fromFile } from "hasha"; -import sharp = require("sharp"); +import * as ExifReader from "exifreader"; +import * as sharp from "sharp"; +import * as fs from "fs/promises"; export async function getHash(file: string): Promise { return await fromFile(file, { @@ -18,6 +20,17 @@ export async function getSize(file: string): Promise { return `${metadata.width}x${metadata.height}`; } +export async function getShotDate(file: string): Promise { + const tags = ExifReader.load(await fs.readFile(file)); + const imageDate = tags["DateTimeOriginal"].description; + if (!imageDate) { + return null; + } + const dateStr = imageDate.split(" ")[0].replace(/:/g, "-"); + const date = new Date(dateStr + "T" + imageDate.split(" ")[1]); + return date; +} + export async function resizeTo( inPath: string, outPath: string,