Files
photos/server/src/services/albums.js
T
2024-04-17 22:54:13 +02:00

298 lines
11 KiB
JavaScript

import {db} from "./db/index.js";
import {Album} from "../models/portfolio/album.js";
import {ApiError, BadRequestError, NotFoundError, ValidationError} from "../models/errors/index.js";
import {jsonPatch} from "../utils/json-patch.js";
import {Photo} from "../models/portfolio/photo.js";
import {minio} from "./minio.js";
import cleanForId from "../utils/clean-for-id.js";
class AlbumsService {
async getAlbums(instanceId) {
return await db.albums.find({instanceId}, {sort: {"sortOrder": 1}}).toArray()
}
async createAlbum(instanceId, album) {
let newAlbum = new Album({
...album,
instanceId,
});
const latestAlbum = await db.albums.findOne({instanceId}, {limit: 1, sort: {"sortOrder": -1}});
newAlbum.albumId = cleanForId(newAlbum.name);
newAlbum.sortOrder = latestAlbum?.sortOrder + 1 || 1;
newAlbum.sort = "auto";
try {
const result = await db.albums.insertOne(newAlbum, {
checkKeys: true,
bypassDocumentValidation: false
});
if (result.acknowledged && result.insertedId) {
return await db.albums.findOne({instanceId, albumId: newAlbum.albumId});
}
throw new BadRequestError("The album could not be created.")
} catch (err) {
if (err instanceof BadRequestError) throw err;
console.error(err);
throw new BadRequestError(err.message);
}
}
async updateAlbum({instanceId, albumId, name, description, sort}) {
const album = await db.albums.findOne({instanceId, albumId});
if (!album) {
throw new NotFoundError("Album not found");
}
album.description = description;
album.sort = sort;
if (album.name !== name) {
album.name = name;
album.albumId = cleanForId(album.name);
}
try {
await db.albums.update(album);
console.log("Album updated", album)
return album;
} catch(err) {
if(err.message?.toLowerCase().indexOf("duplicate key") >= 0) {
throw new BadRequestError("Duplicate name");
}
throw new ApiError(err);
}
}
async patchAlbum(instanceId, albumId, patchOperations) {
const album = await db.albums.findOne({instanceId, albumId});
if (!album) {
throw new NotFoundError("Album not found");
}
const patchPaths = patchOperations.map(op => op.path);
const allowedPaths = ["/name", "/sort", "/description"];
const notAllowedPatches = patchPaths.filter(path => allowedPaths.indexOf(path) < 0)
if(notAllowedPatches.length > 0) {
throw new BadRequestError(`The following paths are invalid: ${notAllowedPatches.map(p => p.path).join(", ")}`)
}
const patchedAlbum = new Album(jsonPatch.applyPatch(album, patchOperations));
if(patchPaths.indexOf("/sort") >= 0) {
if (!["manual", "auto"].includes(patchedAlbum.sort)) {
throw new ValidationError([{
field: "sort",
message: `Parameter 'sort' can only be 'manual' or 'auto', not ${patchedAlbum.sort}.`
}]);
}
}
if(patchPaths.indexOf("/name") >= 0) {
patchedAlbum.albumId = cleanForId(patchedAlbum.name);
}
try {
await db.albums.update(patchedAlbum);
console.log("Album updated", patchedAlbum)
return patchedAlbum;
} catch(err) {
if(err.message?.toLowerCase().indexOf("duplicate key") >= 0) {
throw new BadRequestError("Duplicate name");
}
throw new ApiError(err);
}
}
async deleteAlbum(instanceId, albumId) {
const album = await db.albums.findOne({instanceId, albumId});
if (!album) {
throw new NotFoundError("Album not found");
}
await db.albums.deleteOne({instanceId, albumId});
}
/**
* @param {string} instanceId
* @param {string} albumId
* @param {"up"|"down"}direction
* @return {Promise<boolean>}
*/
async moveAlbum(instanceId, albumId, direction) {
const album = await db.albums.findOne({instanceId, albumId});
if (!album) {
throw new NotFoundError("Album not found");
}
const initialSortOrder = album.sortOrder;
let albumToMove;
albumToMove = await db.albums.findOne({instanceId, sortOrder: initialSortOrder + (direction === "up" ? -1 : 1)});
if (!albumToMove) {
throw new BadRequestError(`Couldn't move album ${direction}, album's sortOrder is ${album.sortOrder}`);
}
album.sortOrder = albumToMove.sortOrder;
albumToMove.sortOrder = initialSortOrder;
await db.albums.update([
album,
albumToMove
]);
return await this.getAlbums();
}
/**
* @typedef FileUploadInfo
* @property {string} fieldname
* @property {string} originalname
* @property {string} encoding
* @property {string} mimetype
* @property {Buffer} buffer
* @property {string} size
*/
allowedMimeTypes = [
"image/*"
];
/**
* @param {string} instanceId
* @param {string} albumId
* @param {FileUploadInfo[]} files
* @return {Promise<Album>}
*/
async uploadPhotos(instanceId, albumId, files) {
const album = await db.albums.findOne({instanceId, albumId});
if (!album) {
throw new NotFoundError("Album not found");
}
const disallowedMimetypeFiles = this.#getDisallowedMimeTypeFiles(this.allowedMimeTypes, files);
if (disallowedMimetypeFiles.length > 0) throw new BadRequestError(`The following files have an incorrect mimetype ${disallowedMimetypeFiles.map(f => `[${f.mimetype}]: ${f.fieldname}`).join("; ")}`);
album.photos = album.photos || [];
let sortOrder = album.photos.at(-1)?.sortOrder + 1 || 1;
// Upload file
/** @type {{photo: Photo, buffer: Buffer}[]} **/
const newPhotos = files.map(file => ({
photo: new Photo({
albumId: albumId,
photoId: cleanForId(file.originalname),
fileName: file.originalname,
mimeType: file.mimetype,
name: file.originalname.substring(0, file.originalname.lastIndexOf(".")),
sortOrder: sortOrder++
}),
buffer: file.buffer
}));
const duplicatePhotos = this.#getDuplicatePhotos(album.photos, newPhotos.map(({photo}) => photo));
if (duplicatePhotos.length > 0) throw new BadRequestError(`The following files already exist ${duplicatePhotos.map(photo => `[${photo.name}]`).join("; ")}, delete the existing photo or rename the file`);
for (let {photo, buffer} of newPhotos) {
photo.s3Path = await minio.uploadFile(buffer, {
path: album.albumId,
name: photo.fileName,
type: photo.mimeType
});
}
album.photos.push(...newPhotos.map(({photo}) => photo));
await db.albums.update(album);
return album;
}
async deletePhoto(instanceId, albumId, photoId) {
const album = await db.albums.findOne({instanceId, albumId});
if (!album) {
throw new NotFoundError("Album not found");
}
const indexToDelete = album.photos?.findIndex(x => x.photoId === photoId)
if (indexToDelete < 0) {
throw new NotFoundError("Photo not found");
}
const photo = album.photos[indexToDelete];
if (photo.s3Path) {
try {
await minio.deleteFile(photo.s3Path);
} catch (_) {
}
}
album.photos = album.photos.filter(photo => photo.photoId !== photoId);
await db.albums.update(album);
return album;
}
async patchPhoto(instanceId, albumId, photoId, patchOperations) {
const album = await db.albums.findOne({instanceId, albumId});
if (!album) {
throw new NotFoundError("Album not found");
}
const photoIndex = album.photos?.findIndex(x => x.photoId === photoId);
if (photoIndex < 0) {
throw new NotFoundError("Photo not found");
}
const patchPaths = patchOperations.map(op => op.path);
const allowedPaths = ["/name", "/description"];
const notAllowedPatches = patchPaths.filter(path => allowedPaths.indexOf(path) < 0)
if(notAllowedPatches.length > 0) {
throw new BadRequestError(`The following paths are invalid: ${notAllowedPatches.map(p => p.path).join(", ")}`)
}
album.photos[photoIndex] = new Photo(jsonPatch.applyPatch(album.photos[photoIndex], patchOperations));
try {
await db.albums.update(album);
console.log("Album updated", album)
return album;
} catch(err) {
if(err.message?.toLowerCase().indexOf("duplicate key") >= 0) {
throw new BadRequestError("Duplicate name");
}
throw new ApiError(err);
}
}
/**
* @param {string} instanceId
* @param {string} albumId
* @param {string} photoId
* @param {"up"|"down"} direction
* @return {Promise<Album>}
*/
async movePhoto(instanceId, albumId, photoId, direction) {
const album = await db.albums.findOne({instanceId, albumId});
if (!album) {
throw new NotFoundError("Album not found");
}
const photoIndex = album.photos?.findIndex(x => x.photoId === photoId);
if (typeof photoIndex === "undefined" || photoIndex < 0) throw new NotFoundError("Photo not found")
const photo = album.photos[photoIndex];
const initialSortOrder = photo.sortOrder;
const photoIndexToMove = album.photos.findIndex(x => x.sortOrder === initialSortOrder + (direction === "up" ? -1 : 1));
if (typeof photoIndexToMove === "undefined" || photoIndexToMove < 0) throw new BadRequestError(`Couldn't move photo ${direction}, photo's sortOrder is ${initialSortOrder}`);
const photoToMove = album.photos[photoIndexToMove];
photo.sortOrder = photoToMove.sortOrder;
photoToMove.sortOrder = initialSortOrder;
album.photos = album.photos.sort((a, b) => a.sortOrder - b.sortOrder);
await db.albums.update(album);
return album;
}
#getDisallowedMimeTypeFiles(allowedTypes, files) {
return files.filter(file => {
const split = file.mimetype.split("/");
return allowedTypes.filter(a => a === file.mimetype || a === `${split[0]}/*`).length === 0;
});
}
#getDuplicatePhotos(existingPhotos, newPhotos) {
return newPhotos.filter(photo =>
existingPhotos.filter(ePhoto => ePhoto.photoId === photo.photoId).length > 0
);
}
}
const instance = new AlbumsService();
export {instance as albums};