auto delete broken photos (without a file)

This commit is contained in:
2022-06-04 20:58:26 +00:00
committed by Stepan Usatiuk
parent 66a2ef2260
commit f02ec38abe
4 changed files with 143 additions and 66 deletions

View File

@@ -47,7 +47,8 @@ export interface IPhotoReqJSON extends IPhotoJSON {
accessToken: string; accessToken: string;
} }
export const ThumbSizes = ["512", "1024", "2048"]; export const thumbSizes = ["512", "1024", "2048", "original"];
export type ThumbSize = typeof thumbSizes[number];
@Entity() @Entity()
@Index(["hash", "size", "user"], { unique: true }) @Index(["hash", "size", "user"], { unique: true })
@@ -72,8 +73,8 @@ export class Photo extends BaseEntity {
@Column({ default: false }) @Column({ default: false })
public uploaded: boolean; public uploaded: boolean;
@Column({ type: "set", enum: ThumbSizes, default: [] }) @Column({ type: "set", enum: thumbSizes, default: [] })
public generatedThumbs: string[]; public generatedThumbs: ThumbSize[];
@Column({ type: "varchar", length: 500 }) @Column({ type: "varchar", length: 500 })
public accessToken: string; 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}.${ return `${this.user.id.toString()}-${this.hash}-${this.size}-${size}.${
mime.extension("image/jpeg") as string mime.extension("image/jpeg") as string
}`; }`;
@@ -112,7 +113,7 @@ export class Photo extends BaseEntity {
return path.join(this.user.getDataPath(), this.getFileName()); 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)); return path.join(this.user.getDataPath(), this.getThumbFileName(size));
} }
@@ -132,37 +133,28 @@ export class Photo extends BaseEntity {
await fs.unlink(this.getPath()); await fs.unlink(this.getPath());
await Promise.all( await Promise.all(
this.generatedThumbs.map( this.generatedThumbs.map(
async (size) => async (size) => await fs.unlink(this.getThumbPath(size)),
await fs.unlink(this.getThumbPath(parseInt(size))),
), ),
); );
} catch (e) { } catch (e) {
if (e.code !== "ENOENT") { if (e.code !== "ENOENT" && e.code !== "NotFoundError") {
throw e; throw e;
} }
} }
} }
// Checks if file exists and updates the DB // Checks if file exists
public async fileExists(): Promise<boolean> { public async origFileExists(): Promise<boolean> {
if (await fileCheck(this.getPath())) { if (await fileCheck(this.getPath())) {
if (!this.uploaded) {
this.uploaded = true;
await this.save();
}
return true; return true;
} else { } else {
if (this.uploaded) {
this.uploaded = false;
this.generatedThumbs = [];
await this.save();
}
return false; return false;
} }
} }
public async processUpload(): Promise<void> { public async processUpload(origFile: string): Promise<void> {
await this.fileExists(); await fs.rename(origFile, this.getPath());
this.uploaded = true;
const date = await getShotDate(this.getPath()); const date = await getShotDate(this.getPath());
if (date !== null) { if (date !== null) {
this.shotAt = date; this.shotAt = date;
@@ -172,26 +164,39 @@ export class Photo extends BaseEntity {
await this.save(); await this.save();
} }
private async generateThumbnail(size: number): Promise<void> { private async generateThumbnail(size: ThumbSize): Promise<void> {
if (!(await this.fileExists())) { if (!(await this.origFileExists())) {
return; await this.remove();
throw new Error("Photo file not found");
} }
await resizeToJpeg(this.getPath(), this.getThumbPath(size), size); await resizeToJpeg(
this.generatedThumbs.push(size.toString()); this.getPath(),
this.getThumbPath(size),
parseInt(size),
);
this.generatedThumbs.push(size);
await this.save(); await this.save();
} }
public async getReadyThumbnailPath(size: number): Promise<string> { public async getReadyPath(size: ThumbSize): Promise<string> {
if (!ThumbSizes.includes(size.toString())) { if (!thumbSizes.includes(size)) {
throw new Error("Wrong thumbnail size"); throw new Error("Wrong thumbnail size");
} }
const path = this.getThumbPath(size); const path =
size === "original" ? this.getPath() : this.getThumbPath(size);
if ( if (
!this.generatedThumbs.includes(size.toString()) || size !== "original" &&
!(await fileCheck(path)) (!this.generatedThumbs.includes(size.toString()) ||
!(await fileCheck(path)))
) { ) {
await this.generateThumbnail(size); await this.generateThumbnail(size);
} }
if (size === "original") {
if (!(await fileCheck(path))) {
await this.remove();
throw new Error("Photo file not found");
}
}
return path; return path;
} }

View File

@@ -86,7 +86,8 @@ export class User extends BaseEntity {
@BeforeRemove() @BeforeRemove()
async removeDataDir(): Promise<void> { async removeDataDir(): Promise<void> {
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() @BeforeInsert()

View File

@@ -94,7 +94,7 @@ photosRouter.post("/photos/upload/:id", async (ctx) => {
return; return;
} }
if (await photo.fileExists()) { if (photo.uploaded) {
ctx.throw(400, "Already uploaded"); ctx.throw(400, "Already uploaded");
return; return;
} }
@@ -120,8 +120,7 @@ photosRouter.post("/photos/upload/:id", async (ctx) => {
try { try {
// TODO: actually move file if it's on different filesystems // TODO: actually move file if it's on different filesystems
await fs.rename(file.path, photo.getPath()); await photo.processUpload(file.path);
await photo.processUpload();
} catch (e) { } catch (e) {
console.log(e); console.log(e);
ctx.throw(500); ctx.throw(500);
@@ -273,7 +272,7 @@ photosRouter.get("/photos/showByID/:id/:token", async (ctx) => {
user: { id: user }, user: { id: user },
}); });
if (!photo || !(await photo.fileExists())) { if (!photo) {
ctx.throw(404); ctx.throw(404);
return; return;
} }
@@ -282,12 +281,12 @@ photosRouter.get("/photos/showByID/:id/:token", async (ctx) => {
ctx.request.query["size"] && ctx.request.query["size"] &&
typeof ctx.request.query["size"] == "string" typeof ctx.request.query["size"] == "string"
) { ) {
const size = parseInt(ctx.request.query["size"]); const size = ctx.request.query["size"];
await send(ctx, await photo.getReadyThumbnailPath(size)); await send(ctx, await photo.getReadyPath(size));
return; return;
} }
await send(ctx, photo.getPath()); await send(ctx, await photo.getReadyPath("original"));
}); });
photosRouter.get("/photos/showByID/:id", async (ctx) => { 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 }); const photo = await Photo.findOne({ id: parseInt(id), user });
if (!photo || !(await photo.fileExists())) { if (!photo) {
ctx.throw(404); ctx.throw(404);
return; return;
} }
@@ -317,12 +316,12 @@ photosRouter.get("/photos/showByID/:id", async (ctx) => {
ctx.request.query["size"] && ctx.request.query["size"] &&
typeof ctx.request.query["size"] == "string" typeof ctx.request.query["size"] == "string"
) { ) {
const size = parseInt(ctx.request.query["size"]); const size = ctx.request.query["size"];
await send(ctx, await photo.getReadyThumbnailPath(size)); await send(ctx, await photo.getReadyPath(size));
return; return;
} }
await send(ctx, photo.getPath()); await send(ctx, await photo.getReadyPath("original"));
}); });
export type IPhotoShowToken = string; export type IPhotoShowToken = string;
@@ -344,7 +343,7 @@ photosRouter.get("/photos/getShowByIDToken/:id", async (ctx) => {
const { user } = ctx.state; const { user } = ctx.state;
const photo = await Photo.findOne({ id: parseInt(id), user }); const photo = await Photo.findOne({ id: parseInt(id), user });
if (!photo || !(await photo.fileExists())) { if (!photo) {
ctx.throw(404); ctx.throw(404);
return; return;
} }

View File

@@ -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 () { it("should show a thumbnail", async function () {
const response = await request(callback) const response = await request(callback)
.get(`/photos/showByID/${seed.dogPhoto.id}?size=512`) .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 () { it("should show a photo using access token", async function () {
const listResp = await request(callback) const listResp = await request(callback)
.get(`/photos/list`) .get(`/photos/list`)
@@ -212,7 +290,7 @@ describe("photos", function () {
}); });
expect(dbPhoto.hash).to.be.equal(dogHash); 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) await request(callback)
.post(`/photos/upload/${photo.id}`) .post(`/photos/upload/${photo.id}`)
@@ -228,7 +306,7 @@ describe("photos", function () {
user: seed.user1.id as any, user: seed.user1.id as any,
}); });
expect(dbPhotoUpl.hash).to.be.equal(dogHash); 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( expect(dbPhotoUpl.shotAt.toISOString()).to.be.equal(
new Date("2020-10-05T14:20:18").toISOString(), new Date("2020-10-05T14:20:18").toISOString(),
); );
@@ -270,7 +348,7 @@ describe("photos", function () {
}); });
expect(dbPhoto.hash).to.be.equal(pngHash); 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) await request(callback)
.post(`/photos/upload/${photo.id}`) .post(`/photos/upload/${photo.id}`)
@@ -287,7 +365,7 @@ describe("photos", function () {
}); });
expect(dbPhotoUpl.hash).to.be.equal(pngHash); expect(dbPhotoUpl.hash).to.be.equal(pngHash);
expect(dbPhotoUpl.format).to.be.equal(pngFormat); 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( expect(dbPhotoUpl.shotAt.getTime()).to.be.approximately(
new Date().getTime(), new Date().getTime(),
10000, 10000,
@@ -367,7 +445,7 @@ describe("photos", function () {
}); });
expect(dbPhoto.hash).to.be.equal(dogHash); 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) await request(callback)
.post(`/photos/upload/${photo.id}`) .post(`/photos/upload/${photo.id}`)
@@ -378,7 +456,7 @@ describe("photos", function () {
.attach("photo", dogPath) .attach("photo", dogPath)
.expect(200); .expect(200);
expect(await dbPhoto.fileExists()).to.be.equal(true); expect(await dbPhoto.origFileExists()).to.be.equal(true);
await request(callback) await request(callback)
.post(`/photos/upload/${photo.id}`) .post(`/photos/upload/${photo.id}`)
@@ -426,7 +504,7 @@ describe("photos", function () {
}); });
expect(dbPhoto.hash).to.be.equal(dogHash); 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) await request(callback)
.post(`/photos/upload/${photo.id}`) .post(`/photos/upload/${photo.id}`)
@@ -437,14 +515,14 @@ describe("photos", function () {
.attach("photo", catPath) .attach("photo", catPath)
.expect(400); .expect(400);
expect(await dbPhoto.fileExists()).to.be.equal(false); expect(await dbPhoto.origFileExists()).to.be.equal(false);
const showResp = await request(callback) const showResp = await request(callback)
.get(`/photos/showByID/${photo.id}`) .get(`/photos/showByID/${photo.id}`)
.set({ .set({
Authorization: `Bearer ${seed.user1.toJWT()}`, Authorization: `Bearer ${seed.user1.toJWT()}`,
}) })
.expect(404); .expect(500);
}); });
it("should create a photo but not upload for other user", async function () { 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, user: seed.user1.id as any,
}); });
expect(dbPhoto.hash).to.be.equal(dogHash); 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) await request(callback)
.post(`/photos/upload/${photo.id}`) .post(`/photos/upload/${photo.id}`)
@@ -482,7 +560,7 @@ describe("photos", function () {
.attach("photo", dogPath) .attach("photo", dogPath)
.expect(404); .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 () { 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, user: seed.user1.id as any,
}); });
expect(dbPhoto.hash).to.be.equal(dogHash); 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) await request(callback)
.post(`/photos/upload/${photo.id}`) .post(`/photos/upload/${photo.id}`)
@@ -520,7 +598,7 @@ describe("photos", function () {
.attach("photo", dogPath) .attach("photo", dogPath)
.expect(200); .expect(200);
expect(await dbPhoto.fileExists()).to.be.equal(true); expect(await dbPhoto.origFileExists()).to.be.equal(true);
await request(callback) await request(callback)
.get(`/photos/showByID/${photo.id}`) .get(`/photos/showByID/${photo.id}`)
@@ -618,9 +696,7 @@ describe("photos", function () {
it("should delete a photo", async function () { it("should delete a photo", async function () {
const photoPath = seed.dogPhoto.getPath(); const photoPath = seed.dogPhoto.getPath();
const photoSmallThumbPath = await seed.dogPhoto.getReadyThumbnailPath( const photoSmallThumbPath = await seed.dogPhoto.getReadyPath("512");
512,
);
const response = await request(callback) const response = await request(callback)
.post(`/photos/delete`) .post(`/photos/delete`)
.set({ .set({
@@ -648,12 +724,8 @@ describe("photos", function () {
it("should delete two photos", async function () { it("should delete two photos", async function () {
const photo1Path = seed.dogPhoto.getPath(); const photo1Path = seed.dogPhoto.getPath();
const photo2Path = seed.catPhoto.getPath(); const photo2Path = seed.catPhoto.getPath();
const photo1SmallThumbPath = await seed.dogPhoto.getReadyThumbnailPath( const photo1SmallThumbPath = await seed.dogPhoto.getReadyPath("512");
512, const photo2SmallThumbPath = await seed.catPhoto.getReadyPath("512");
);
const photo2SmallThumbPath = await seed.catPhoto.getReadyThumbnailPath(
512,
);
const response = await request(callback) const response = await request(callback)
.post(`/photos/delete`) .post(`/photos/delete`)
.set({ .set({