Initial commit. Files are uploaded to the filesystem.

This commit is contained in:
ThePendulum
2025-09-25 06:19:37 +02:00
commit 745b00dcdc
64 changed files with 9572 additions and 0 deletions

174
dist/api.js vendored Normal file
View File

@@ -0,0 +1,174 @@
import { parse } from '@brillout/json-serializer/parse'; /* eslint-disable-line import/extensions */
import events from '#src/events';
const postHeaders = {
mode: 'cors',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
};
function getQuery(data) {
if (!data) {
return '';
}
const curatedQuery = Object.fromEntries(Object.entries(data).map(([key, value]) => (value === undefined ? null : [key, value])).filter(Boolean));
return `?${new URLSearchParams(curatedQuery).toString()}`; // recode so commas aren't encoded
}
function showFeedback(isSuccess, options = {}, errorMessage) {
if (!isSuccess && (typeof options.errorFeedback === 'string' || options.appendErrorMessage)) {
events.emit('feedback', {
type: 'error',
message: options.appendErrorMessage && errorMessage
? `${options.errorFeedback ? `${options.errorFeedback}: ` : ''}${errorMessage}`
: options.errorFeedback,
});
return;
}
if (!isSuccess) {
events.emit('feedback', {
type: 'error',
message: 'Error, please try again',
});
return;
}
if (isSuccess && options.successFeedback) {
events.emit('feedback', {
type: 'success',
message: options.successFeedback,
});
return;
}
if (isSuccess && options.undoFeedback) {
events.emit('feedback', {
type: 'undo',
message: options.undoFeedback,
});
}
}
export async function get(path, query = {}, options = {}) {
try {
const res = await fetch(`/api${path}${getQuery(query)}`);
const body = parse(await res.text());
if (res.ok) {
showFeedback(true, options);
return body;
}
showFeedback(false, options, body.statusMessage);
throw new Error(body.statusMessage);
}
catch (error) {
showFeedback(false, options, error.message);
throw error;
}
}
export async function post(path, data, options = {}) {
try {
const isForm = data instanceof FormData;
console.log('POST', isForm, data);
const curatedData = !data || isForm
? data
: JSON.stringify(data);
const res = await fetch(`/api${path}${getQuery(options.query)}`, {
method: 'POST',
body: curatedData,
/*
...postHeaders,
headers: {
...postHeaders.headers,
...(isForm ? { 'Content-Type': 'multipart/form-data' } : {}),
},
*/
});
if (res.status === 204) {
showFeedback(true, options);
return null;
}
const body = parse(await res.text());
if (res.ok) {
showFeedback(true, options);
return body;
}
showFeedback(false, options, body.statusMessage);
throw new Error(body.statusMessage);
}
catch (error) {
showFeedback(false, options, error.message);
throw error;
}
}
export async function postForm(path, form, options = {}) {
try {
const res = await fetch(`/api${path}${getQuery(options.query)}`, {
method: 'POST',
...postHeaders,
headers: {
'Content-Type': 'multipart/form-data',
},
body: form,
});
if (res.status === 204) {
showFeedback(true, options);
return null;
}
const body = parse(await res.text());
if (res.ok) {
showFeedback(true, options);
return body;
}
showFeedback(false, options, body.statusMessage);
throw new Error(body.statusMessage);
}
catch (error) {
showFeedback(false, options, error.message);
throw error;
}
}
export async function patch(path, data, options = {}) {
try {
const res = await fetch(`/api${path}${getQuery(options.query)}`, {
method: 'PATCH',
body: data && JSON.stringify(data),
...postHeaders,
});
if (res.status === 204) {
showFeedback(true, options);
return null;
}
const body = parse(await res.text());
if (res.ok) {
showFeedback(true, options);
return body;
}
showFeedback(false, options, body.statusMessage);
throw new Error(body.statusMessage);
}
catch (error) {
showFeedback(false, options, error.message);
throw error;
}
}
export async function del(path, options = {}) {
try {
const res = await fetch(`/api${path}${getQuery(options.query)}`, {
method: 'DELETE',
body: options.data && JSON.stringify(options.data),
...postHeaders,
});
if (res.status === 204) {
showFeedback(true, options);
return null;
}
const body = parse(await res.text());
if (res.ok) {
showFeedback(true, options);
return body;
}
showFeedback(false, options, body.statusMessage);
throw new Error(body.statusMessage);
}
catch (error) {
showFeedback(false, options, error.message);
throw error;
}
}
//# sourceMappingURL=api.js.map

1
dist/api.js.map vendored Normal file

File diff suppressed because one or more lines are too long

13
dist/app.js vendored Normal file
View File

@@ -0,0 +1,13 @@
import config from 'config';
import path from 'path';
import fs from 'fs/promises';
import { initServer } from '#src/web/server.js';
async function init() {
if (config.uploads.flushTempOnStart) {
await fs.rmdir(path.join(config.uploads.path, 'temp'), { recursive: true });
await fs.mkdir(path.join(config.uploads.path, 'temp'));
}
await initServer();
}
init();
//# sourceMappingURL=app.js.map

1
dist/app.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,QAAQ,CAAC;AAC5B,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,MAAM,aAAa,CAAC;AAE7B,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAEhD,KAAK,UAAU,IAAI;IAClB,IAAI,MAAM,CAAC,OAAO,CAAC,gBAAgB,EAAE,CAAC;QACrC,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC5E,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;IACxD,CAAC;IAED,MAAM,UAAU,EAAE,CAAC;AACpB,CAAC;AAED,IAAI,EAAE,CAAC"}

14
dist/errors.js vendored Normal file
View File

@@ -0,0 +1,14 @@
export class HttpError extends Error {
constructor(message, httpCode, friendlyMessage, data) {
super(message);
this.name = 'HttpError';
this.httpCode = httpCode;
if (friendlyMessage) {
this.friendlyMessage = friendlyMessage;
}
if (data) {
this.data = data;
}
}
}
//# sourceMappingURL=errors.js.map

1
dist/errors.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.js"],"names":[],"mappings":"AAAA,MAAM,OAAO,SAAU,SAAQ,KAAK;IACnC,YAAY,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,IAAI;QACnD,KAAK,CAAC,OAAO,CAAC,CAAC;QAEf,IAAI,CAAC,IAAI,GAAG,WAAW,CAAC;QACxB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QAEzB,IAAI,eAAe,EAAE,CAAC;YACrB,IAAI,CAAC,eAAe,GAAG,eAAe,CAAC;QACxC,CAAC;QAED,IAAI,IAAI,EAAE,CAAC;YACV,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QAClB,CAAC;IACF,CAAC;CACD"}

3
dist/events.js vendored Normal file
View File

@@ -0,0 +1,3 @@
import mitt from 'mitt';
export default mitt();
//# sourceMappingURL=events.js.map

1
dist/events.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"events.js","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,eAAe,IAAI,EAAE,CAAC"}

46
dist/files.js vendored Normal file
View File

@@ -0,0 +1,46 @@
import config from 'config';
import path from 'path';
import fs from 'fs/promises';
import mime from 'mime';
import crypto from 'crypto';
import { fileTypeFromBuffer } from 'file-type';
import sharp from 'sharp';
import { HttpError } from '#src/errors.js';
function hashFile(fileBuffer) {
return crypto.createHash('sha256')
.update(fileBuffer)
.digest('hex');
}
async function validateMimetype(file, fileBuffer) {
const { mime } = await fileTypeFromBuffer(fileBuffer);
if (mime !== file.mimetype) {
throw new HttpError(`MIME type mismatch: ${file.mimetype} expected, ${mime} found`, 400);
}
}
async function generateThumb(type, fileBuffer, hash, targetPath) {
if (type === 'image') {
await sharp(fileBuffer)
.resize({ height: config.uploads.thumbHeight })
.jpeg({ quality: config.uploads.thumbQuality })
.toFile(path.join(targetPath, `${hash}_t.jpg`));
}
}
export async function uploadFiles(files, options) {
console.log('FILES', files, options);
await files.reduce(async (chain, file) => {
await chain;
const tempFilePath = path.join(config.uploads.path, 'temp', file.tempName);
const fileBuffer = await fs.readFile(tempFilePath);
const hash = hashFile(fileBuffer);
await validateMimetype(file, fileBuffer);
const targetPath = path.join(config.uploads.path, 'full', hash.slice(0, 2), hash.slice(2, 4));
const extension = mime.getExtension(file.mimetype);
await fs.mkdir(targetPath, { recursive: true });
await Promise.all([
generateThumb(file.mimetype.split('/')[0], fileBuffer, hash, targetPath),
fs.rename(path.join(config.uploads.path, 'temp', file.tempName), path.join(targetPath, `${hash}.${extension}`)),
]);
}, Promise.resolve());
return files;
}
//# sourceMappingURL=files.js.map

1
dist/files.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"files.js","sourceRoot":"","sources":["../src/files.ts"],"names":[],"mappings":"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"}

13
dist/web/files.js vendored Normal file
View File

@@ -0,0 +1,13 @@
import { uploadFiles } from '#src/files.js';
export async function uploadFilesApi(req, res) {
const files = req.files;
const uploads = await uploadFiles(files.map((file) => ({
fileName: file.originalname,
encoding: file.encoding,
mimetype: file.mimetype,
tempName: file.filename,
size: file.size,
})), JSON.parse(req.body.options));
res.send(uploads);
}
//# sourceMappingURL=files.js.map

1
dist/web/files.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"files.js","sourceRoot":"","sources":["../../src/web/files.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE5C,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,GAAY,EAAE,GAAa;IAC/D,MAAM,KAAK,GAAG,GAAG,CAAC,KAA8B,CAAC;IAEjD,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QACrD,QAAQ,EAAE,IAAI,CAAC,YAAY;QAC3B,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,IAAI,EAAE,IAAI,CAAC,IAAI;KAChB,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;IAEnC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;AACnB,CAAC"}

5
dist/web/root.js vendored Normal file
View File

@@ -0,0 +1,5 @@
import { dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
export const root = `${__dirname}/../..`;
//# sourceMappingURL=root.js.map

1
dist/web/root.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"root.js","sourceRoot":"","sources":["../../src/web/root.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAC/B,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AACpC,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAE1D,MAAM,CAAC,MAAM,IAAI,GAAG,GAAG,SAAS,QAAQ,CAAC"}

55
dist/web/server.js vendored Normal file
View File

@@ -0,0 +1,55 @@
import config from 'config';
import express from 'express';
import bodyParser from 'express';
import compression from 'compression';
import { renderPage, createDevMiddleware } from 'vike/server';
import multer from 'multer';
import { root } from '#web/root.js';
import { uploadFilesApi } from '#web/files.js';
const isProduction = process.env.NODE_ENV === 'production';
export async function initServer() {
const app = express();
// const upload = multer({ dest: './uploads' });
const upload = multer({ dest: './uploads/temp' });
app.use(compression());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded());
// Vite integration
if (isProduction) {
// In production, we need to serve our static assets ourselves.
// (In dev, Vite's middleware serves our static assets.)
const sirv = (await import('sirv')).default;
app.use(sirv(`${root}/dist/client`));
}
else {
const { devMiddleware } = await createDevMiddleware({ root });
app.use(devMiddleware);
}
// app.post('/api/files', uploadFilesApi);
app.post('/api/files', upload.array('files'), uploadFilesApi);
// Vike middleware. It should always be our last middleware (because it's a
// catch-all middleware superseding any middleware placed after it).
app.get('*', async (req, res) => {
const pageContextInit = {
urlOriginal: req.originalUrl,
headersOriginal: req.headers
};
const pageContext = await renderPage(pageContextInit);
if (pageContext.errorWhileRendering) {
// Install error tracking here, see https://vike.dev/error-tracking
}
const { httpResponse } = pageContext;
if (res.writeEarlyHints)
res.writeEarlyHints({ link: httpResponse.earlyHints.map((e) => e.earlyHintLink) });
httpResponse.headers.forEach(([name, value]) => res.setHeader(name, value));
res.status(httpResponse.statusCode);
// For HTTP streams use pageContext.httpResponse.pipe() instead, see https://vike.dev/streaming
res.send(httpResponse.body);
});
const host = process.env.HOST || config.web.host || 'localhost';
const port = process.env.PORT || config.web.port || 3000;
app.listen(port, host, () => {
console.log(`Server running at http://${host}:${port}`);
});
}
//# sourceMappingURL=server.js.map

1
dist/web/server.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/web/server.ts"],"names":[],"mappings":"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"}