diff --git a/src/entity/Photo.ts b/src/entity/Photo.ts index a324199..e94312e 100644 --- a/src/entity/Photo.ts +++ b/src/entity/Photo.ts @@ -47,7 +47,8 @@ export interface IPhotoReqJSON extends IPhotoJSON { accessToken: string; } -export const ThumbSizes = ["512", "1024", "2048"]; +export const thumbSizes = ["512", "1024", "2048", "original"]; +export type ThumbSize = typeof thumbSizes[number]; @Entity() @Index(["hash", "size", "user"], { unique: true }) @@ -72,8 +73,8 @@ export class Photo extends BaseEntity { @Column({ default: false }) public uploaded: boolean; - @Column({ type: "set", enum: ThumbSizes, default: [] }) - public generatedThumbs: string[]; + @Column({ type: "set", enum: thumbSizes, default: [] }) + public generatedThumbs: ThumbSize[]; @Column({ type: "varchar", length: 500 }) public accessToken: string; @@ -102,7 +103,7 @@ export class Photo extends BaseEntity { }`; } - private getThumbFileName(size: number): string { + private getThumbFileName(size: ThumbSize): string { return `${this.user.id.toString()}-${this.hash}-${this.size}-${size}.${ mime.extension("image/jpeg") as string }`; @@ -112,7 +113,7 @@ export class Photo extends BaseEntity { return path.join(this.user.getDataPath(), this.getFileName()); } - private getThumbPath(size: number): string { + public getThumbPath(size: ThumbSize): string { return path.join(this.user.getDataPath(), this.getThumbFileName(size)); } @@ -132,37 +133,28 @@ export class Photo extends BaseEntity { await fs.unlink(this.getPath()); await Promise.all( this.generatedThumbs.map( - async (size) => - await fs.unlink(this.getThumbPath(parseInt(size))), + async (size) => await fs.unlink(this.getThumbPath(size)), ), ); } catch (e) { - if (e.code !== "ENOENT") { + if (e.code !== "ENOENT" && e.code !== "NotFoundError") { throw e; } } } - // Checks if file exists and updates the DB - public async fileExists(): Promise { + // Checks if file exists + public async origFileExists(): Promise { if (await fileCheck(this.getPath())) { - if (!this.uploaded) { - this.uploaded = true; - await this.save(); - } return true; } else { - if (this.uploaded) { - this.uploaded = false; - this.generatedThumbs = []; - await this.save(); - } return false; } } - public async processUpload(): Promise { - await this.fileExists(); + public async processUpload(origFile: string): Promise { + await fs.rename(origFile, this.getPath()); + this.uploaded = true; const date = await getShotDate(this.getPath()); if (date !== null) { this.shotAt = date; @@ -172,26 +164,39 @@ export class Photo extends BaseEntity { await this.save(); } - private async generateThumbnail(size: number): Promise { - if (!(await this.fileExists())) { - return; + private async generateThumbnail(size: ThumbSize): Promise { + if (!(await this.origFileExists())) { + await this.remove(); + throw new Error("Photo file not found"); } - await resizeToJpeg(this.getPath(), this.getThumbPath(size), size); - this.generatedThumbs.push(size.toString()); + await resizeToJpeg( + this.getPath(), + this.getThumbPath(size), + parseInt(size), + ); + this.generatedThumbs.push(size); await this.save(); } - public async getReadyThumbnailPath(size: number): Promise { - if (!ThumbSizes.includes(size.toString())) { + public async getReadyPath(size: ThumbSize): Promise { + if (!thumbSizes.includes(size)) { throw new Error("Wrong thumbnail size"); } - const path = this.getThumbPath(size); + const path = + size === "original" ? this.getPath() : this.getThumbPath(size); if ( - !this.generatedThumbs.includes(size.toString()) || - !(await fileCheck(path)) + size !== "original" && + (!this.generatedThumbs.includes(size.toString()) || + !(await fileCheck(path))) ) { await this.generateThumbnail(size); } + if (size === "original") { + if (!(await fileCheck(path))) { + await this.remove(); + throw new Error("Photo file not found"); + } + } return path; } diff --git a/src/entity/User.ts b/src/entity/User.ts index 868ed0d..9b47b34 100644 --- a/src/entity/User.ts +++ b/src/entity/User.ts @@ -86,7 +86,8 @@ export class User extends BaseEntity { @BeforeRemove() async removeDataDir(): Promise { - await fs.rmdir(this.getDataPath(), { recursive: true }); + // force because otherwise it will fail if directory already doesn't exist + await fs.rm(this.getDataPath(), { recursive: true, force: true }); } @BeforeInsert() diff --git a/src/routes/photos.ts b/src/routes/photos.ts index d861360..de0bf03 100644 --- a/src/routes/photos.ts +++ b/src/routes/photos.ts @@ -94,7 +94,7 @@ photosRouter.post("/photos/upload/:id", async (ctx) => { return; } - if (await photo.fileExists()) { + if (photo.uploaded) { ctx.throw(400, "Already uploaded"); return; } @@ -120,8 +120,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(); + await photo.processUpload(file.path); } catch (e) { console.log(e); ctx.throw(500); @@ -273,7 +272,7 @@ photosRouter.get("/photos/showByID/:id/:token", async (ctx) => { user: { id: user }, }); - if (!photo || !(await photo.fileExists())) { + if (!photo) { ctx.throw(404); return; } @@ -282,12 +281,12 @@ photosRouter.get("/photos/showByID/:id/:token", async (ctx) => { ctx.request.query["size"] && typeof ctx.request.query["size"] == "string" ) { - const size = parseInt(ctx.request.query["size"]); - await send(ctx, await photo.getReadyThumbnailPath(size)); + const size = ctx.request.query["size"]; + await send(ctx, await photo.getReadyPath(size)); return; } - await send(ctx, photo.getPath()); + await send(ctx, await photo.getReadyPath("original")); }); photosRouter.get("/photos/showByID/:id", async (ctx) => { @@ -308,7 +307,7 @@ photosRouter.get("/photos/showByID/:id", async (ctx) => { const photo = await Photo.findOne({ id: parseInt(id), user }); - if (!photo || !(await photo.fileExists())) { + if (!photo) { ctx.throw(404); return; } @@ -317,12 +316,12 @@ photosRouter.get("/photos/showByID/:id", async (ctx) => { ctx.request.query["size"] && typeof ctx.request.query["size"] == "string" ) { - const size = parseInt(ctx.request.query["size"]); - await send(ctx, await photo.getReadyThumbnailPath(size)); + const size = ctx.request.query["size"]; + await send(ctx, await photo.getReadyPath(size)); return; } - await send(ctx, photo.getPath()); + await send(ctx, await photo.getReadyPath("original")); }); export type IPhotoShowToken = string; @@ -344,7 +343,7 @@ photosRouter.get("/photos/getShowByIDToken/:id", async (ctx) => { const { user } = ctx.state; const photo = await Photo.findOne({ id: parseInt(id), user }); - if (!photo || !(await photo.fileExists())) { + if (!photo) { ctx.throw(404); return; } diff --git a/src/tests/integration/photos.test.ts b/src/tests/integration/photos.test.ts index 268d765..023b60c 100644 --- a/src/tests/integration/photos.test.ts +++ b/src/tests/integration/photos.test.ts @@ -91,6 +91,55 @@ describe("photos", function () { ); }); + it("should delete a photo after file has been deleted", async function () { + const response = await request(callback) + .get(`/photos/showByID/${seed.dogPhoto.id}`) + .set({ + Authorization: `Bearer ${seed.user2.toJWT()}`, + }) + .expect(200); + expect(parseInt(response.header["content-length"])).to.equal( + dogFileSize, + ); + + await fs.unlink(await seed.dogPhoto.getReadyPath("original")); + const response2 = await request(callback) + .get(`/photos/showByID/${seed.dogPhoto.id}`) + .set({ + Authorization: `Bearer ${seed.user2.toJWT()}`, + }) + .expect(500); + const dbPhoto = await Photo.findOne(seed.dogPhoto.id); + expect(dbPhoto).to.be.undefined; + }); + + it("should delete a photo after file has been deleted (thumbnail access)", async function () { + const response = await request(callback) + .get(`/photos/showByID/${seed.dogPhoto.id}?size=512`) + .set({ + Authorization: `Bearer ${seed.user2.toJWT()}`, + }) + .expect(200); + const dogSmallThumbSize = ( + await fs.stat(await seed.dogPhoto.getThumbPath("512")) + ).size; + expect(parseInt(response.header["content-length"])).to.equal( + dogSmallThumbSize, + ); + + await fs.unlink(await seed.dogPhoto.getReadyPath("512")); + await fs.unlink(await seed.dogPhoto.getReadyPath("original")); + const response2 = await request(callback) + .get(`/photos/showByID/${seed.dogPhoto.id}?size=512`) + .set({ + Authorization: `Bearer ${seed.user2.toJWT()}`, + }) + .expect(500); + + const dbPhoto = await Photo.findOne(seed.dogPhoto.id); + expect(dbPhoto).to.be.undefined; + }); + it("should show a thumbnail", async function () { const response = await request(callback) .get(`/photos/showByID/${seed.dogPhoto.id}?size=512`) @@ -103,6 +152,35 @@ describe("photos", function () { ); }); + it("should show a thumbnail after it was deleted", async function () { + const response = await request(callback) + .get(`/photos/showByID/${seed.dogPhoto.id}?size=512`) + .set({ + Authorization: `Bearer ${seed.user2.toJWT()}`, + }) + .expect(200); + const dogSmallThumbSize = ( + await fs.stat(await seed.dogPhoto.getThumbPath("512")) + ).size; + expect(parseInt(response.header["content-length"])).to.equal( + dogSmallThumbSize, + ); + + await fs.unlink(seed.dogPhoto.getThumbPath("512")); + const response2 = await request(callback) + .get(`/photos/showByID/${seed.dogPhoto.id}?size=512`) + .set({ + Authorization: `Bearer ${seed.user2.toJWT()}`, + }) + .expect(200); + const dogSmallThumbSize2 = ( + await fs.stat(await seed.dogPhoto.getThumbPath("512")) + ).size; + expect(parseInt(response.header["content-length"])).to.equal( + dogSmallThumbSize2, + ); + }); + it("should show a photo using access token", async function () { const listResp = await request(callback) .get(`/photos/list`) @@ -212,7 +290,7 @@ describe("photos", function () { }); expect(dbPhoto.hash).to.be.equal(dogHash); - expect(await dbPhoto.fileExists()).to.be.equal(false); + expect(await dbPhoto.origFileExists()).to.be.equal(false); await request(callback) .post(`/photos/upload/${photo.id}`) @@ -228,7 +306,7 @@ describe("photos", function () { user: seed.user1.id as any, }); expect(dbPhotoUpl.hash).to.be.equal(dogHash); - expect(await dbPhotoUpl.fileExists()).to.be.equal(true); + expect(await dbPhotoUpl.origFileExists()).to.be.equal(true); expect(dbPhotoUpl.shotAt.toISOString()).to.be.equal( new Date("2020-10-05T14:20:18").toISOString(), ); @@ -270,7 +348,7 @@ describe("photos", function () { }); expect(dbPhoto.hash).to.be.equal(pngHash); - expect(await dbPhoto.fileExists()).to.be.equal(false); + expect(await dbPhoto.origFileExists()).to.be.equal(false); await request(callback) .post(`/photos/upload/${photo.id}`) @@ -287,7 +365,7 @@ describe("photos", function () { }); expect(dbPhotoUpl.hash).to.be.equal(pngHash); expect(dbPhotoUpl.format).to.be.equal(pngFormat); - expect(await dbPhotoUpl.fileExists()).to.be.equal(true); + expect(await dbPhotoUpl.origFileExists()).to.be.equal(true); expect(dbPhotoUpl.shotAt.getTime()).to.be.approximately( new Date().getTime(), 10000, @@ -367,7 +445,7 @@ describe("photos", function () { }); expect(dbPhoto.hash).to.be.equal(dogHash); - expect(await dbPhoto.fileExists()).to.be.equal(false); + expect(await dbPhoto.origFileExists()).to.be.equal(false); await request(callback) .post(`/photos/upload/${photo.id}`) @@ -378,7 +456,7 @@ describe("photos", function () { .attach("photo", dogPath) .expect(200); - expect(await dbPhoto.fileExists()).to.be.equal(true); + expect(await dbPhoto.origFileExists()).to.be.equal(true); await request(callback) .post(`/photos/upload/${photo.id}`) @@ -426,7 +504,7 @@ describe("photos", function () { }); expect(dbPhoto.hash).to.be.equal(dogHash); - expect(await dbPhoto.fileExists()).to.be.equal(false); + expect(await dbPhoto.origFileExists()).to.be.equal(false); await request(callback) .post(`/photos/upload/${photo.id}`) @@ -437,14 +515,14 @@ describe("photos", function () { .attach("photo", catPath) .expect(400); - expect(await dbPhoto.fileExists()).to.be.equal(false); + expect(await dbPhoto.origFileExists()).to.be.equal(false); const showResp = await request(callback) .get(`/photos/showByID/${photo.id}`) .set({ Authorization: `Bearer ${seed.user1.toJWT()}`, }) - .expect(404); + .expect(500); }); it("should create a photo but not upload for other user", async function () { @@ -471,7 +549,7 @@ describe("photos", function () { user: seed.user1.id as any, }); expect(dbPhoto.hash).to.be.equal(dogHash); - expect(await dbPhoto.fileExists()).to.be.equal(false); + expect(await dbPhoto.origFileExists()).to.be.equal(false); await request(callback) .post(`/photos/upload/${photo.id}`) @@ -482,7 +560,7 @@ describe("photos", function () { .attach("photo", dogPath) .expect(404); - expect(await dbPhoto.fileExists()).to.be.equal(false); + expect(await dbPhoto.origFileExists()).to.be.equal(false); }); it("should create, upload but not show a photo to another user", async function () { @@ -509,7 +587,7 @@ describe("photos", function () { user: seed.user1.id as any, }); expect(dbPhoto.hash).to.be.equal(dogHash); - expect(await dbPhoto.fileExists()).to.be.equal(false); + expect(await dbPhoto.origFileExists()).to.be.equal(false); await request(callback) .post(`/photos/upload/${photo.id}`) @@ -520,7 +598,7 @@ describe("photos", function () { .attach("photo", dogPath) .expect(200); - expect(await dbPhoto.fileExists()).to.be.equal(true); + expect(await dbPhoto.origFileExists()).to.be.equal(true); await request(callback) .get(`/photos/showByID/${photo.id}`) @@ -618,9 +696,7 @@ describe("photos", function () { it("should delete a photo", async function () { const photoPath = seed.dogPhoto.getPath(); - const photoSmallThumbPath = await seed.dogPhoto.getReadyThumbnailPath( - 512, - ); + const photoSmallThumbPath = await seed.dogPhoto.getReadyPath("512"); const response = await request(callback) .post(`/photos/delete`) .set({ @@ -648,12 +724,8 @@ describe("photos", function () { it("should delete two photos", async function () { const photo1Path = seed.dogPhoto.getPath(); const photo2Path = seed.catPhoto.getPath(); - const photo1SmallThumbPath = await seed.dogPhoto.getReadyThumbnailPath( - 512, - ); - const photo2SmallThumbPath = await seed.catPhoto.getReadyThumbnailPath( - 512, - ); + const photo1SmallThumbPath = await seed.dogPhoto.getReadyPath("512"); + const photo2SmallThumbPath = await seed.catPhoto.getReadyPath("512"); const response = await request(callback) .post(`/photos/delete`) .set({