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 fe7cb57..fda3bce 100644 Binary files a/src/tests/integration/photos/cat.jpg and b/src/tests/integration/photos/cat.jpg differ diff --git a/src/tests/integration/photos/dog.jpg b/src/tests/integration/photos/dog.jpg index ac32d2f..f04ef5c 100644 Binary files a/src/tests/integration/photos/dog.jpg and b/src/tests/integration/photos/dog.jpg differ 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,