From fd96e8bd595dac0f361963de14ecab83f4d51555 Mon Sep 17 00:00:00 2001 From: JiPaix <26584973+JiPaix@users.noreply.github.com> Date: Sun, 7 May 2023 00:30:35 +0200 Subject: [PATCH] fix(api): cached files are duplicated (same content stored twice) --- packages/api/src/index.ts | 39 +++++++++++++++++++++++++++++- packages/api/src/utils/fileserv.ts | 33 ++++++++++++++++++++++++- 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index f3e2626f9..21ae261a7 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -8,7 +8,7 @@ import history from 'connect-history-api-fallback'; import cors from 'cors'; import express from 'express'; import morgan from 'morgan'; -import { join } from 'path'; +import { join, resolve } from 'path'; import { env } from 'process'; export default function useFork(settings: ForkEnv = env):Promise<{ client: client, fork:Fork }> { @@ -65,6 +65,26 @@ export default function useFork(settings: ForkEnv = env):Promise<{ client: clien }); }); + // serve the files + app.get('/cache/:folderName/:fileName', (req, res, next) => { + const options = { + root: resolve(fileServer.cacheFolder, req.params.folderName), + dotfiles: 'deny', + headers: { + 'x-timestamp': Date.now(), + 'x-sent': true, + }, + }; + const fileName = req.params.fileName; + res.sendFile(fileName, options, (err) => { + if (err) { + next(err); + } else { + fileServer.renew(fileName); + } + }); + }); + // force authentication for file requests app.use('/files', (req, res, next) => { const b64auth = (req.headers.authorization || '').split(' ')[1] || ''; @@ -82,6 +102,23 @@ export default function useFork(settings: ForkEnv = env):Promise<{ client: clien } }); + // force authentication for cache requests + app.use('/cache', (req, res, next) => { + const b64auth = (req.headers.authorization || '').split(' ')[1] || ''; + const strauth = Buffer.from(b64auth, 'base64').toString(); + const splitIndex = strauth.indexOf(':'); + const login = strauth.substring(0, splitIndex); + const password = strauth.substring(splitIndex + 1); + if(login !== env.LOGIN || password !== env.PASSWORD) { + // Access denied... + res.set('WWW-Authenticate', 'Basic realm="401"'); // change this + res.status(401).send('Authentication required.'); // custom message + } else { + // Access granted... + next(); + } + }); + // serve the view if any if(env.VIEW) { app.use(express.static(env.VIEW)); diff --git a/packages/api/src/utils/fileserv.ts b/packages/api/src/utils/fileserv.ts index 58f6b9bc4..9244126cd 100644 --- a/packages/api/src/utils/fileserv.ts +++ b/packages/api/src/utils/fileserv.ts @@ -1,10 +1,26 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from 'fs'; -import { join, resolve } from 'path'; +import { basename, dirname, join, resolve } from 'path'; import { env } from 'process'; import sanitize from 'sanitize-filename'; + + +function* getFiles(dir: string):Generator { + const dirents = readdirSync(dir, { withFileTypes: true }); + for (const dirent of dirents.filter(f => f.name !== 'fileserver')) { + const res = resolve(dir, dirent.name); + if (dirent.isDirectory()) { + yield* getFiles(res); + } else { + yield res; + } + } +} + + export class FileServer { static #instance: FileServer; folder: string; + cacheFolder: string; /** default file's lifetime in seconds */ #defaultLifeTime = 60*60*24; #timeouts: {filename: string, timeout: NodeJS.Timeout}[]; @@ -15,6 +31,7 @@ export class FileServer { constructor(folder:string) { if(!env.USER_DATA) throw new Error('USER_DATA not set'); this.folder = resolve(env.USER_DATA, '.cache', folder); + this.cacheFolder = resolve(env.USER_DATA, '.cache'); this.#timeouts = []; this.setup(); this.empty(); @@ -63,6 +80,10 @@ export class FileServer { return resolve(this.folder, sanitize(filename)); } + #resolveFilesFromCache() { + return getFiles(this.cacheFolder); + } + #resetFile(filename: string, lifetime: number) { const path = this.#resolveFile(filename); const fileExist = existsSync(path); @@ -101,6 +122,13 @@ export class FileServer { } } + #findFromCache(filename: string) { + const files = this.#resolveFilesFromCache(); + for(const f of files) { + if(f.includes(sanitize(filename))) return f; + } + } + /** * * @param data the data to serv @@ -108,6 +136,9 @@ export class FileServer { * @param lifetime how lang is the file available (in seconds) */ serv (data: Buffer, filename:string, lifetime = this.#defaultLifeTime) { + const cached = this.#findFromCache(filename); + if(cached) return `/cache/${basename(dirname(cached))}/${filename}`; + const exist = this.#resetFile(filename, lifetime); if(exist) return `/files/${filename}`;