Enforce max views on access and purge after the last allowed view.
This commit is contained in:
@@ -320,6 +320,56 @@ describe('UploadService', () => {
|
||||
expect(storageService.read).not.toHaveBeenCalled();
|
||||
expect(uploadRepository.increment).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('throws NotFoundException when maxViews is exhausted', async () => {
|
||||
uploadRepository.findOne.mockResolvedValue({
|
||||
storagePath: fixedId,
|
||||
iv: Buffer.alloc(12, 0x11),
|
||||
maxViews: 1,
|
||||
viewCount: 1,
|
||||
zk: {
|
||||
passwordProtected: false,
|
||||
wrappedDek: null,
|
||||
wrapIv: null,
|
||||
salt: null
|
||||
}
|
||||
});
|
||||
|
||||
await expect(service.getZkUploadCiphertext(fixedId)).rejects.toThrow(NotFoundException);
|
||||
expect(storageService.read).not.toHaveBeenCalled();
|
||||
expect(uploadRepository.increment).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('purges upload after the last allowed view', async () => {
|
||||
const iv = Buffer.alloc(12, 0x11);
|
||||
const ciphertext = Buffer.alloc(64, 0xcd);
|
||||
const upload = {
|
||||
id: fixedId,
|
||||
storagePath: fixedId,
|
||||
iv,
|
||||
maxViews: 1,
|
||||
viewCount: 0,
|
||||
zk: {
|
||||
passwordProtected: false,
|
||||
wrappedDek: null,
|
||||
wrapIv: null,
|
||||
salt: null
|
||||
}
|
||||
};
|
||||
|
||||
uploadRepository.findOne.mockResolvedValue(upload);
|
||||
storageService.read.mockResolvedValue(ciphertext);
|
||||
|
||||
await expect(service.getZkUploadCiphertext(fixedId)).resolves.toEqual({
|
||||
iv: iv.toString('base64'),
|
||||
ciphertext: ciphertext.toString('base64'),
|
||||
passwordProtected: false
|
||||
});
|
||||
|
||||
expect(uploadRepository.increment).toHaveBeenCalledWith({ id: fixedId }, 'viewCount', 1);
|
||||
expect(storageService.unlink).toHaveBeenCalledWith(fixedId);
|
||||
expect(uploadRepository.remove).toHaveBeenCalledWith(upload);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getHotlinkImage', () => {
|
||||
@@ -395,6 +445,63 @@ describe('UploadService', () => {
|
||||
expect(storageService.read).not.toHaveBeenCalled();
|
||||
expect(uploadRepository.increment).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('throws NotFoundException when maxViews is exhausted', async () => {
|
||||
uploadRepository.findOne.mockResolvedValue({
|
||||
storagePath: fixedId,
|
||||
iv,
|
||||
maxViews: 2,
|
||||
viewCount: 2,
|
||||
hotlink: { mimeType: 'image/png' }
|
||||
});
|
||||
|
||||
await expect(service.getHotlinkImage(fixedId, secret)).rejects.toThrow(NotFoundException);
|
||||
expect(storageService.read).not.toHaveBeenCalled();
|
||||
expect(uploadRepository.increment).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('purges upload after the last allowed view', async () => {
|
||||
const upload = {
|
||||
id: fixedId,
|
||||
storagePath: fixedId,
|
||||
iv,
|
||||
maxViews: 1,
|
||||
viewCount: 0,
|
||||
hotlink: { mimeType: 'image/png' }
|
||||
};
|
||||
|
||||
uploadRepository.findOne.mockResolvedValue(upload);
|
||||
storageService.read.mockResolvedValue(ciphertext);
|
||||
|
||||
await expect(service.getHotlinkImage(fixedId, secret)).resolves.toEqual({
|
||||
body: plaintext,
|
||||
mimeType: 'image/png'
|
||||
});
|
||||
|
||||
expect(uploadRepository.increment).toHaveBeenCalledWith({ id: fixedId }, 'viewCount', 1);
|
||||
expect(storageService.unlink).toHaveBeenCalledWith(fixedId);
|
||||
expect(uploadRepository.remove).toHaveBeenCalledWith(upload);
|
||||
});
|
||||
|
||||
it('does not purge when views remain below maxViews', async () => {
|
||||
uploadRepository.findOne.mockResolvedValue({
|
||||
storagePath: fixedId,
|
||||
iv,
|
||||
maxViews: 3,
|
||||
viewCount: 0,
|
||||
hotlink: { mimeType: 'image/png' }
|
||||
});
|
||||
storageService.read.mockResolvedValue(ciphertext);
|
||||
|
||||
await expect(service.getHotlinkImage(fixedId, secret)).resolves.toEqual({
|
||||
body: plaintext,
|
||||
mimeType: 'image/png'
|
||||
});
|
||||
|
||||
expect(uploadRepository.increment).toHaveBeenCalled();
|
||||
expect(storageService.unlink).not.toHaveBeenCalled();
|
||||
expect(uploadRepository.remove).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteUpload', () => {
|
||||
|
||||
@@ -132,6 +132,10 @@ export class UploadService {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (typeof upload.maxViews === 'number' && upload.viewCount >= upload.maxViews) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
let ciphertextBuffer: Buffer;
|
||||
|
||||
try {
|
||||
@@ -162,6 +166,12 @@ export class UploadService {
|
||||
|
||||
await this.uploadRepository.increment({ id }, 'viewCount', 1);
|
||||
|
||||
const nextViewCount = upload.viewCount + 1;
|
||||
|
||||
if (typeof upload.maxViews === 'number' && nextViewCount >= upload.maxViews) {
|
||||
await this.purgeUpload(upload, this.uploadRepository);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -179,6 +189,10 @@ export class UploadService {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (typeof upload.maxViews === 'number' && upload.viewCount >= upload.maxViews) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
let ciphertextBuffer: Buffer;
|
||||
|
||||
try {
|
||||
@@ -203,6 +217,12 @@ export class UploadService {
|
||||
|
||||
await this.uploadRepository.increment({ id }, 'viewCount', 1);
|
||||
|
||||
const nextViewCount = upload.viewCount + 1;
|
||||
|
||||
if (typeof upload.maxViews === 'number' && nextViewCount >= upload.maxViews) {
|
||||
await this.purgeUpload(upload, this.uploadRepository);
|
||||
}
|
||||
|
||||
return {
|
||||
body,
|
||||
mimeType: upload.hotlink.mimeType
|
||||
@@ -221,6 +241,11 @@ export class UploadService {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await this.purgeUpload(upload, uploadRepository);
|
||||
});
|
||||
}
|
||||
|
||||
private async purgeUpload(upload: Upload, uploadRepository: Repository<Upload>): Promise<void> {
|
||||
try {
|
||||
await this.storageService.unlink(upload.storagePath);
|
||||
} catch (error: unknown) {
|
||||
@@ -230,7 +255,6 @@ export class UploadService {
|
||||
}
|
||||
|
||||
await uploadRepository.remove(upload);
|
||||
});
|
||||
}
|
||||
|
||||
private hashDeleteToken(token: string): string {
|
||||
|
||||
Reference in New Issue
Block a user