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;
}
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<boolean> {
// Checks if file exists
public async origFileExists(): Promise<boolean> {
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<void> {
await this.fileExists();
public async processUpload(origFile: string): Promise<void> {
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<void> {
if (!(await this.fileExists())) {
return;
private async generateThumbnail(size: ThumbSize): Promise<void> {
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<string> {
if (!ThumbSizes.includes(size.toString())) {
public async getReadyPath(size: ThumbSize): Promise<string> {
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;
}

View File

@@ -86,7 +86,8 @@ export class User extends BaseEntity {
@BeforeRemove()
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()

View File

@@ -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;
}

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 () {
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({