const mime = require("mime-types"); const fs = require('fs'); const Path = require('path'); const Security = require('../src/security.js'); const MediaService = require('../model/mediaService.js'); const MediaFileMetaModel = require('../model/mediaItemMeta.js').MediaFileMetaModel; const MediaFileTagModel = require('../model/mediaItemTag.js').MediaFileTagModel; const { AccessModel, ACCESS_TYPE, ACCESS_GRANT, ACCESS_TO } = require('../model/access.js'); function MediaToJson(mediaData) { if (!mediaData) return null; if (mediaData.accessType === ACCESS_GRANT.readNoMeta) mediaData.meta = { height: mediaData.meta?.height, width: mediaData.meta?.width }; return mediaData; } function accessToJson(access) { const typeStr = [ "unknown", "ldapAccount", "email", "link", "every one" ][access.type]; const accessToStr = [ "unknown", "item", "tag", "meta", "everything", "admin"][access.accessTo]; const grantStr = [ "none", "read", "write", "read without meta"][access.grant]; return { id: access.id, type: typeStr, typeLabel: access.typeLabel, typeData: access.typeData, accessTo: accessToStr, accessToData: access.accessToData, grant: grantStr }; } async function accessListToJson(app, req) { let result = { ...(req.sessionObj?.accessList || {}) }; result.isAdmin = await req.sessionObj?.accessList?.isAdmin?.(app, result) || false; delete result.isAdmin_; return result; } module.exports = { register: app => { app.router.post("/api/server/reboot", async (req, res) => { app.routerUtils.onApiRequest(req, res); if (!await req.sessionObj?.accessList?.isAdmin(app, req.sessionObj?.accessList)) return app.routerUtils.onBadRequest(res); console.log("Starting reboot process, initiaed from ", res.sessionObj); app.server.close(() => { require("child_process").spawn(process.argv.shift(), process.argv, { cwd: process.cwd(), detached : true, stdio: "inherit" }).unref(); setTimeout(() => process.exit(), 500); }); app.routerUtils.jsonResponse(res, {}); }); app.router.post("/api/database/reload", async (req, res) => { app.routerUtils.onApiRequest(req, res); if (!await req.sessionObj?.accessList?.isAdmin(app, req.sessionObj?.accessList)) return app.routerUtils.onBadRequest(res); app.libraryManager.forceReload(app); app.routerUtils.jsonResponse(res, {}); }); app.router.post("/api/database/scan", async (req, res) => { app.routerUtils.onApiRequest(req, res); if (!await req.sessionObj?.accessList?.isAdmin(app, req.sessionObj?.accessList)) return app.routerUtils.onBadRequest(res); app.libraryManager.updateLibraries(app); app.routerUtils.jsonResponse(res, {}); }); app.router.get("/api/access/list", async (req, res) => { app.routerUtils.onApiRequest(req, res); app.routerUtils.jsonResponse(res, await accessListToJson(app, req)); }); app.router.post("/api/access/link", async (req, res) => { // /api/access/link, post: { linkIds: [string] (JSON) } app.routerUtils.onApiRequest(req, res); if (!req.post?.linkIds?.length) return app.routerUtils.httpResponse(res, 400, "Missing argument"); try { for (let i of JSON.parse(req.post.linkIds)) { const access = await app.databaseHelper.findOne(AccessModel, { type: ACCESS_TYPE.link, typeData: i }); if (access) { Security.addLinkToSession(req, access.id, i, access.typeLabel); if (access.accessTo == ACCESS_TO.admin) Security.setAdmin(req, true); } } } catch (err) { console.error(err); return app.routerUtils.onBadRequest(res); } app.routerUtils.jsonResponse(res, await accessListToJson(app, req)); }); app.router.del("/api/access/:id", async (req, res) => { app.routerUtils.onApiRequest(req, res); Security.removeFromSession(req, req.params.id); const access = await app.databaseHelper.fetch(AccessModel, { id: Object.keys(req.sessionObj.accessList).map(i => req.sessionObj.accessList[i]).filter(x => x.dbId).map(x => x.dbId), accessTo: ACCESS_TO.admin }); const result = Security.setAdmin(req, !!(access?.length || 0)); app.routerUtils.jsonResponse(res, result); }); app.router.post("/api/accessAdmin/create", async (req, res) => { app.routerUtils.onApiRequest(req, res); if (!await req.sessionObj?.accessList?.isAdmin(app, req.sessionObj?.accessList) || !req.body) return app.routerUtils.onBadRequest(res); let access = new AccessModel(); access.type = parseInt(req.body.typeId); access.typeData = req.body.typeData; access.typeLabel = req.body.typeLabel; access.accessTo = parseInt(req.body.accessToId); access.accessToData = req.body.accessToData; access.grant = parseInt(req.body.grant); try { await app.databaseHelper.insertOne(access); } catch (err) { console.error(err); return app.routerUtils.onBadRequest(res); } app.routerUtils.jsonResponse(res, accessToJson(access)); }); app.router.del("/api/accessAdmin/:id", async (req, res) => { app.routerUtils.onApiRequest(req, res); if (!await req.sessionObj?.accessList?.isAdmin(app, req.sessionObj?.accessList) || !req.params.id) return app.routerUtils.onBadRequest(res); app.databaseHelper.remove(AccessModel, { id: parseInt(req.params.id) }); app.routerUtils.jsonResponse(res, {}); }); app.router.post("/api/accessAdmin/:id", async (req, res) => { app.routerUtils.onApiRequest(req, res); if (!await req.sessionObj?.accessList?.isAdmin(app, req.sessionObj?.accessList) || !req.params.id || !req.body) return app.routerUtils.onBadRequest(res); const access = (await app.databaseHelper.fetch(AccessModel, { id: parseInt(req.params.id) }))?.[0]; if (!access) return app.routerUtils.onBadRequest(res); access.typeLabel = req.body.typeLabel; access.typeData = req.body.typeData; access.accessTo = parseInt(req.body.accessToId); access.accessToData = req.body.accessToData; access.grant = parseInt(req.body.grant); try { app.databaseHelper.upsertOne(access); } catch (err) { console.error(err); return app.routerUtils.onBadRequest(res); } app.routerUtils.jsonResponse(res, accessToJson(access)); }); app.router.get("/api/accessAdmin/list", async (req, res) => { app.routerUtils.onApiRequest(req, res); if (!await req.sessionObj?.accessList?.isAdmin(app, req.sessionObj?.accessList)) return app.routerUtils.onBadRequest(res); app.routerUtils.jsonResponse(res, (await app.databaseHelper.fetch(AccessModel)).map(accessToJson)); }); app.router.post("/api/media/:id/tag/del/:tag", async (req, res) => { app.routerUtils.onApiRequest(req, res); if (!req.params.id ||!req.params.tag) return app.routerUtils.onBadRequest(res); let checksum = [ req.params.id ]; if (req.params.id === "list") { if (!req.body?.['list[]']) return app.routerUtils.onBadRequest(res); checksum = req.body['list[]']; } let data = await MediaService.fetchMultiple(app, checksum, req.sessionObj?.accessList, 0); data = Object.keys(data).map(x => data[x]).filter(x => x.ACCESS_TYPE != ACCESS_GRANT.write); await Promise.all(data.map(x => MediaService.updateVersionInDb(app, x.fixedSum))); await app.databaseHelper.remove(MediaFileTagModel, { md5sum: data.map(x => x.fixedSum), tag: decodeURIComponent(req.params.tag), fromMeta: 0 }); const allMedias = await MediaService.fetchMultiple(app, checksum, req.sessionObj?.accessList, 0); app.routerUtils.jsonResponse(res, Object.keys(allMedias).map(x => allMedias[x]).map(x => MediaToJson(x))); }); app.router.put("/api/media/:id/tag", async (req, res) => { app.routerUtils.onApiRequest(req, res); const requestedTag = req.body?.tag; if (!req.params.id ||!requestedTag) return app.routerUtils.onBadRequest(res); let checksum = [ req.params.id ]; if (req.params.id === "list") { if (!req.body?.['list[]']) return app.routerUtils.onBadRequest(res); checksum = req.body['list[]']; } let data = await MediaService.fetchMultiple(app, checksum, req.sessionObj?.accessList, 0); data = Object.keys(data) .map(x => data[x]) .filter(x => { if (x.ACCESS_TYPE != ACCESS_GRANT.write) return true; for (let existingTag of [...x.tags, ...x.fixedTags]) { if (existingTag === requestedTag || existingTag.startsWith(`${requestedTag}/`)) { return true; } } }); await Promise.all(data.map(x => MediaService.updateVersionInDb(app, x.fixedSum))); let tag = data.map(x => new MediaFileTagModel(x.fixedSum, requestedTag, false)); try { await app.databaseHelper.insertMultipleSameTable(tag); } catch (err) { console.error(err); return app.routerUtils.onBadRequest(res); } const allMedias = await MediaService.fetchMultiple(app, checksum, req.sessionObj?.accessList, 0); app.routerUtils.jsonResponse(res, Object.keys(allMedias).map(x => allMedias[x]).map(x => MediaToJson(x))); }); app.router.patch("/api/media/:id/meta/:key", async (req, res) => { app.routerUtils.onApiRequest(req, res); if (!req.params.id ||!req.params.key || !Number.isInteger(req.body?.value?.length)) return app.routerUtils.onBadRequest(res); let checksum = [ req.params.id ]; if (req.params.id === "list") { if (!req.body?.['list[]']) return app.routerUtils.onBadRequest(res); checksum = req.body['list[]']; } let data = await MediaService.fetchMultiple(app, checksum, req.sessionObj?.accessList, 0); data = Object.keys(data) .map(x => data[x]) .filter(x => x.ACCESS_TYPE != ACCESS_GRANT.write); if (!(await MediaService.updateMeta(app, data.map(x => x.fixedSum), req.params.key, req.body.value))) return app.routerUtils.onBadRequest(res); const allMedias = await MediaService.fetchMultiple(app, checksum, req.sessionObj?.accessList, 0); app.routerUtils.jsonResponse(res, Object.keys(allMedias).map(x => allMedias[x]).map(x => MediaToJson(x))); }); app.router.get("/api/media/list", async (req, res) => { app.routerUtils.onApiRequest(req, res); let first = undefined, last = undefined, maxVersion = undefined; if (req.body?.chronology !== undefined) { let range = await MediaService.getMediaRange(app); first = range.min; last = range.max; maxVersion = range.maxVersion; } let fromDate = parseInt(req.body?.from); let count = parseInt(req.body?.count); app.routerUtils.jsonResponse(res, { data: (await MediaService.fetchMediasWithAccess( app, isNaN(fromDate) ? 0 : fromDate, isNaN(count) ? 25 : Math.min(350, count), req.sessionObj?.accessList, req.body?.version || 0)).map(MediaToJson), first: first, last: last, maxVersion: maxVersion }); }); app.router.get("/api/media/sumlist", async (req, res) => { app.routerUtils.onApiRequest(req, res); app.routerUtils.jsonResponse(res, { data: await MediaService.fetchMediasSumWithAccess( app, req.sessionObj?.accessList) }); }); app.router.del("/api/media/:md5sum", async (req, res) => { app.routerUtils.onApiRequest(req, res); let data = MediaToJson(await MediaService.fetchOne(app, req.params.md5sum, req.sessionObj?.accessList, 0)); if (!data) return app.routerUtils.onPageNotFound(res); await MediaService.removeMedia(app, data); app.routerUtils.jsonResponse(res, {}); }); app.router.get("/api/media/:md5sum", async (req, res) => { app.routerUtils.onApiRequest(req, res); let data = MediaToJson(await MediaService.fetchOne(app, req.params.md5sum, req.sessionObj?.accessList, 0)); if (!data) return app.routerUtils.onPageNotFound(res); app.routerUtils.jsonResponse(res, data); }); app.router.get("/api/media/thumbnail/:md5sum.jpg", async (req, res) => { app.routerUtils.onApiRequest(req, res); let data = await MediaService.fetchOne(app, req.params.md5sum, req.sessionObj?.accessList, 0); if (!data) return app.routerUtils.onPageNotFound(res); try { let thumbnail = null; req.body = req.body || {}; req.body.w = parseInt(req.body.w || 0); req.body.h = parseInt(req.body.h || 0); req.body.q = parseInt(req.body.q || 6); try { thumbnail = await (await app.libraryManager.findMedia(data.path))?.createThumbnail(req.body.w, req.body.h, req.body.q); } catch (err) { return app.routerUtils.apiError(res); } if (!thumbnail) return app.routerUtils.onPageNotFound(res); res.setHeader("Content-Type", "image/jpeg"); res.setHeader("Content-Length", fs.statSync(thumbnail.name)?.size || undefined); res.setHeader("Cache-Control", "private, max-age=2630000"); // 1 month cache let rd = fs.createReadStream(thumbnail.name); rd.once('end', () => thumbnail.removeCallback()); rd.pipe(res); } catch (err) { console.error(err); app.routerUtils.onPageNotFound(res); } }); app.router.get("/api/media/original/:md5sum", async (req, res) => { app.routerUtils.onApiRequest(req, res); let data = await MediaService.fetchOne(app, req.params.md5sum, req.sessionObj?.accessList, 0); if (!data) return app.routerUtils.onPageNotFound(res); const fileName = Path.basename(data.path); res.setHeader("Cache-Control", "private, max-age=2630000"); // 1 month cache if (data.accessType === ACCESS_GRANT.readNoMeta || req.body?.trim !== undefined) { console.log("remove meta");//-> trim metadata } res.setHeader("Content-Disposition", `attachment; filename="${fileName}"`); res.setHeader("Content-Type", mime.lookup(data.path)); res.setHeader("Content-Length", fs.statSync(data.path)?.size || undefined); fs.createReadStream(data.path).pipe(res); }); }};