mirror of
https://github.com/usatiuk/photos.git
synced 2025-10-28 15:27:49 +01:00
auto delete broken photos (without a file)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user