added nextcloud
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
import express from 'express';
|
||||
import * as multer from 'multer';
|
||||
import fs from 'fs/promises';
|
||||
import fsSync from 'fs';
|
||||
import { createClient } from 'webdav';
|
||||
import { pathKey, fanoutPath, ensureDir, getRow, upsertRow, CACHE_DIR } from './cache.ts';
|
||||
import { checkIfHasPerms, statOne, toRegExp } from './helpers.ts';
|
||||
import path from 'path';
|
||||
|
||||
type statRetType = {
|
||||
"filename": string,
|
||||
"basename": string,
|
||||
"lastmod": string,
|
||||
"size": number,
|
||||
"type": "file" | "directory",
|
||||
"etag": string,
|
||||
"mime"?: string
|
||||
};
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
fobj?: {
|
||||
fpath: any,
|
||||
deep?: boolean,
|
||||
bypasscache?: boolean
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
NEXTCLOUD_APP_ID: APP_ID,
|
||||
NEXTCLOUD_APP_PASS: APP_PASS,
|
||||
NEXTCLOUD_WEBDAV_ADDR: ADDR,
|
||||
NEXTCLOUD_ACCESS_DIRS: _DIRS,
|
||||
PORT: PORT_RAW
|
||||
} = process.env,
|
||||
PORT = PORT_RAW || 1111;
|
||||
|
||||
if (!(ADDR && APP_ID && APP_PASS)) {
|
||||
throw new Error("VARIABLES NOT FOUND IN ENV");
|
||||
}
|
||||
|
||||
// const ADDR_URL = new URL(ADDR);
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use(express.text());
|
||||
app.use((await import('cors')).default());
|
||||
|
||||
app.use((req, res, next) => {
|
||||
if (req.path === '/openapi.json') {
|
||||
return res.sendFile('openapi.json', { root: '.' });
|
||||
}
|
||||
|
||||
// GET has no body (independant handlers)
|
||||
else if (req.method === 'GET') return next();
|
||||
|
||||
const pth = checkIfHasPerms(req);
|
||||
if (!pth && req.body?.fpath) {
|
||||
return res.status(401).send(`Not allowed to access "${req.body.fpath}"`);
|
||||
}
|
||||
else if (!pth) {
|
||||
return res.status(404).send("Unknown path");
|
||||
}
|
||||
|
||||
req.fobj = pth;
|
||||
next();
|
||||
});
|
||||
|
||||
//@ts-ignore stupid
|
||||
const multerInstance = multer.default({
|
||||
storage: multer.diskStorage({
|
||||
destination: CACHE_DIR
|
||||
})
|
||||
});
|
||||
|
||||
export const DIRS: string[] = (() => {
|
||||
try {
|
||||
if (!_DIRS) return null;
|
||||
return JSON.parse(_DIRS).map(toRegExp)
|
||||
}
|
||||
catch (err) { console.error(err); }
|
||||
return null;
|
||||
})();
|
||||
|
||||
if (!DIRS) {
|
||||
console.warn("NEXTCLOUD_ACCESS_DIRS not specified, tool has full access to all files");
|
||||
}
|
||||
|
||||
const client = createClient(process.env.NEXTCLOUD_WEBDAV_ADDR!, {
|
||||
username: process.env.NEXTCLOUD_APP_ID!,
|
||||
password: process.env.NEXTCLOUD_APP_PASS!,
|
||||
});
|
||||
|
||||
app.post('/file', async (req, res) => {
|
||||
if (!req.fobj) return res.sendStatus(401);
|
||||
const { fpath, bypasscache } = req.fobj;
|
||||
|
||||
try {
|
||||
// read current etag/size from webdav
|
||||
const { etag, size, mime } = await statOne(fpath, client).catch((err) => {
|
||||
if (typeof err === 'string') {
|
||||
console.error(err);
|
||||
res.sendStatus(404).send(err);
|
||||
return { etag: undefined, size: -1, mime: -1 };
|
||||
}
|
||||
|
||||
throw err;
|
||||
});
|
||||
|
||||
if (!etag) return;
|
||||
|
||||
// lookup mapping
|
||||
const key = pathKey(process.env.NEXTCLOUD_WEBDAV_ADDR!, fpath),
|
||||
cache_path = fanoutPath(key),
|
||||
row = bypasscache ? null : getRow(key);
|
||||
|
||||
// cached file
|
||||
if (row && row.etag === etag) {
|
||||
const data = await fs.readFile(row.cache_path);
|
||||
res.setHeader('content-type', row.mime ?? 'application/octet-stream');
|
||||
return res.send(data);
|
||||
}
|
||||
|
||||
// TODO: If-None-Match here if client lib exposes raw headers
|
||||
const buf = await client.getFileContents(fpath).catch(console.error);
|
||||
if (!buf) return res.sendStatus(500);
|
||||
|
||||
await ensureDir(cache_path);
|
||||
await fs.writeFile(cache_path, buf as any);
|
||||
|
||||
upsertRow({
|
||||
path_key: key,
|
||||
fpath,
|
||||
etag,
|
||||
size,
|
||||
mime: mime ?? null,
|
||||
cache_path,
|
||||
updated_at: Date.now()
|
||||
});
|
||||
|
||||
res.setHeader('content-type', mime ?? 'application/octet-stream');
|
||||
res.send(buf);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.sendStatus(500);
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/dir', async (req, res) => {
|
||||
try {
|
||||
if (!req.fobj) return res.sendStatus(401);
|
||||
|
||||
const { fpath, deep } = req.fobj,
|
||||
filesInDir = await client.getDirectoryContents(fpath, {
|
||||
deep
|
||||
});
|
||||
|
||||
res.json(filesInDir);
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/file', multerInstance.single('file'), async (req, res) => {
|
||||
if (!req.fobj) return res.sendStatus(401);
|
||||
if (!req.file) return res.status(400).send("missing file");
|
||||
|
||||
const { fdir, createnewdirs } = req.body as {
|
||||
fdir: string,
|
||||
createnewdirs: boolean
|
||||
};
|
||||
|
||||
if (createnewdirs && !(await client.exists(fdir))) {
|
||||
await client.createDirectory(fdir, {
|
||||
recursive: true
|
||||
});
|
||||
}
|
||||
|
||||
const fname = req.file.filename || req.file.originalname,
|
||||
fpath = path.join(fdir, fname);
|
||||
|
||||
if (await client.exists(fpath)) {
|
||||
return res.status(503).send('file already exists, you do not have permissions to overwrite files');
|
||||
}
|
||||
|
||||
const sstr = fsSync.createReadStream(req.file.path).pipe(client.createWriteStream(fpath, {
|
||||
overwrite: false
|
||||
}, (r) => {
|
||||
if (res.headersSent) return;
|
||||
res.send(`file uploaded successfully to ${r.url}`);
|
||||
}));
|
||||
|
||||
sstr.on('error', (err) => {
|
||||
console.error(err);
|
||||
if (res.headersSent) return;
|
||||
|
||||
res.status(500).send("failed to upload file");
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/ping', (_, res) => res.sendStatus(200));
|
||||
|
||||
app.listen(PORT, (err) => {
|
||||
if (err) throw err;
|
||||
console.log(`app listening on port ${PORT}`);
|
||||
});
|
||||
Reference in New Issue
Block a user