added nextcloud
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
npm-cache
|
||||
bun.lock
|
||||
bun.lockb
|
||||
*.log
|
||||
@@ -0,0 +1,17 @@
|
||||
FROM oven/bun:1 AS base
|
||||
WORKDIR /app
|
||||
|
||||
# prod deps
|
||||
COPY package.json ./package.json
|
||||
|
||||
RUN bun install --ci --production
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN mkdir -p /app/data && chown -R bun:bun /app/data
|
||||
|
||||
USER bun
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
CMD ["bun", "run", "server.ts"]
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Database } from 'bun:sqlite';
|
||||
import { createHash } from 'crypto';
|
||||
import path from 'path';
|
||||
import fs from 'fs/promises';
|
||||
|
||||
type CacheRow = {
|
||||
path_key: string,
|
||||
fpath: string,
|
||||
etag: string,
|
||||
size: number,
|
||||
mime: string | null,
|
||||
cache_path: string,
|
||||
updated_at: number
|
||||
};
|
||||
|
||||
// TODO: CHANGEME WHEN RUNNING IN DOCKER
|
||||
export const CACHE_DIR = "/tmp",
|
||||
db = new Database(path.resolve(`${CACHE_DIR}/index.sqlite`));
|
||||
|
||||
db.run(`
|
||||
create table if not exists file_cache (
|
||||
path_key text primary key,
|
||||
fpath text not null,
|
||||
etag text not null,
|
||||
size integer not null,
|
||||
mime text,
|
||||
cache_path text not null,
|
||||
updated_at integer not null
|
||||
);
|
||||
`);
|
||||
|
||||
const qGet = db.query<CacheRow, string>('select * from file_cache where path_key = ?;');
|
||||
|
||||
const qUpsert = db.query(`
|
||||
insert into file_cache (path_key, fpath, etag, size, mime, cache_path, updated_at)
|
||||
values (?1, ?2, ?3, ?4, ?5, ?6, ?7)
|
||||
on conflict(path_key) do update set
|
||||
etag=excluded.etag, size=excluded.size, mime=excluded.mime,
|
||||
cache_path=excluded.cache_path, updated_at=excluded.updated_at;
|
||||
`);
|
||||
|
||||
export const base64url = (buf: Buffer): string =>
|
||||
buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); // no padding
|
||||
|
||||
export const pathKey = (addr: string, fpath: string): string => {
|
||||
// normalize to avoid duplicate keys for equivalent paths
|
||||
const canonical = new URL(fpath, addr).toString(),
|
||||
digest = createHash('sha256').update(canonical).digest();
|
||||
|
||||
return base64url(digest);
|
||||
};
|
||||
|
||||
export const fanoutPath = (key: string): string =>
|
||||
path.join(`${CACHE_DIR}/files`, key.slice(0, 2), key.slice(2, 4), key);
|
||||
|
||||
export async function ensureDir(p: string): Promise<void> {
|
||||
await fs.mkdir(path.dirname(p), { recursive: true });
|
||||
}
|
||||
|
||||
export function getRow(key: string): CacheRow | undefined {
|
||||
return qGet.get(key) as unknown as CacheRow | undefined;
|
||||
}
|
||||
|
||||
export function upsertRow(row: CacheRow): void {
|
||||
qUpsert.run(row.path_key, row.fpath, row.etag, row.size, row.mime, row.cache_path, row.updated_at);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Request } from "express";
|
||||
import { WebDAVClient } from "webdav";
|
||||
import { DIRS } from "./server";
|
||||
|
||||
|
||||
export async function statOne(fpath: string, client: WebDAVClient) {
|
||||
const parent = fpath.replace(/\/[^/]+$/, '') || '/',
|
||||
base = fpath.split('/').filter(Boolean).pop(),
|
||||
list = await client.getDirectoryContents(parent, { deep: false }),
|
||||
entry = Array.isArray(list) ? list.find((e: any) => e.basename === base) : undefined;
|
||||
|
||||
if (!entry) throw `not found: ${fpath}`;
|
||||
return { etag: entry.etag as string, size: Number(entry.size), mime: entry.mime as string | undefined };
|
||||
}
|
||||
|
||||
export const toRegExp = (spec: string): RegExp | null => {
|
||||
// slash-delimited form: /pattern/flags
|
||||
const m = spec.match(/^\/(.+)\/([a-z]*)$/i);
|
||||
try {
|
||||
if (m) {
|
||||
const [, source, flags] = m;
|
||||
return new RegExp(source, flags);
|
||||
}
|
||||
|
||||
// raw pattern form: "pattern"
|
||||
return new RegExp(spec);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function checkIfHasPerms(req: Request) {
|
||||
if (!req.body) {
|
||||
return false;
|
||||
}
|
||||
const { fpath, obj_type, deep, usecache } = req.body,
|
||||
o = { fpath, obj_type, deep, usecache };
|
||||
|
||||
if (!fpath) return false;
|
||||
if (!DIRS) return o;
|
||||
if (fpath.includes('..')) return false;
|
||||
|
||||
return DIRS.find(r => fpath.match(r)) ? o : false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,503 @@
|
||||
{
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "Nextcloud Files API (openwebui-friendly)",
|
||||
"description": "simple file/directory access via nextcloud webdav, with local disk cache.",
|
||||
"version": "1.0.1"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "http://ollama-nextcloud:1111",
|
||||
"description": "local server (use absolute url for tool discovery)"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/ping": {
|
||||
"get": {
|
||||
"operationId": "nextcloudPing",
|
||||
"summary": "health / verification probe",
|
||||
"description": "simple json probe used by tool registrars to verify the server is reachable",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "ok",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ok": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"time": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"example": "2025-09-01T12:00:00Z"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"ok"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/file": {
|
||||
"post": {
|
||||
"operationId": "nextcloudGetFile",
|
||||
"summary": "Fetch a file (proxied via WebDAV, cached locally)",
|
||||
"description": "returns the raw file bytes. content-type mirrors the upstream mime when available; otherwise application/octet-stream. also supports an application/json metadata variant for tool registration and LLM-friendly responses.",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fpath": {
|
||||
"type": "string",
|
||||
"description": "absolute path in nextcloud webdav (e.g., /Documents/report.pdf)"
|
||||
},
|
||||
"bypasscache": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "if true, skip the local cache and fetch from upstream"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fpath"
|
||||
]
|
||||
},
|
||||
"examples": {
|
||||
"default": {
|
||||
"value": {
|
||||
"fpath": "/Documents/report.pdf",
|
||||
"bypasscache": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "file bytes or metadata",
|
||||
"headers": {
|
||||
"content-type": {
|
||||
"description": "mime type from upstream or application/octet-stream",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/FileMeta"
|
||||
},
|
||||
"examples": {
|
||||
"example": {
|
||||
"value": {
|
||||
"filename": "/Documents/report.pdf",
|
||||
"basename": "report.pdf",
|
||||
"lastmod": "Mon, 01 Sep 2025 12:34:56 GMT",
|
||||
"size": 2048,
|
||||
"type": "file",
|
||||
"etag": "\"a1b2c3d4e5\"",
|
||||
"mime": "application/pdf",
|
||||
"cached": true,
|
||||
"download_url": "http://ollama-nextcloud:1111/file?fpath=/Documents/report.pdf"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"application/octet-stream": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "binary"
|
||||
}
|
||||
},
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "binary"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/Unauthorized"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/components/responses/NotFound"
|
||||
},
|
||||
"500": {
|
||||
"$ref": "#/components/responses/ServerError"
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"operationId": "nextcloudUploadFile",
|
||||
"summary": "Upload a file",
|
||||
"description": "uploads a file into a target directory in nextcloud via webdav. requires a multipart/form-data body with the file part and the destination directory. the server will not overwrite existing files.",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fdir": {
|
||||
"type": "string",
|
||||
"description": "destination directory path (e.g., /Documents)"
|
||||
},
|
||||
"createnewdirs": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "if true and the directory does not exist, create it recursively before uploading"
|
||||
},
|
||||
"file": {
|
||||
"type": "string",
|
||||
"format": "binary",
|
||||
"description": "the file contents"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fdir",
|
||||
"file"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "upload succeeded",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ok": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"uploaded": {
|
||||
"$ref": "#/components/schemas/DirEntry"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"ok",
|
||||
"uploaded"
|
||||
]
|
||||
}
|
||||
},
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": "file uploaded successfully to https://nextcloud.example.com/remote.php/dav/files/user/Documents/example.txt"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "missing file upload field",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ok": {
|
||||
"type": "boolean",
|
||||
"example": false
|
||||
},
|
||||
"error": {
|
||||
"type": "string",
|
||||
"example": "missing file"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": "missing file"
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/Unauthorized"
|
||||
},
|
||||
"503": {
|
||||
"description": "file exists and overwrite is not permitted",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ok": {
|
||||
"type": "boolean",
|
||||
"example": false
|
||||
},
|
||||
"error": {
|
||||
"type": "string",
|
||||
"example": "file already exists, you do not have permissions to overwrite files"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": "file already exists, you do not have permissions to overwrite files"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"$ref": "#/components/responses/ServerError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/dir": {
|
||||
"post": {
|
||||
"operationId": "nextcloudListDirectory",
|
||||
"summary": "List a directory",
|
||||
"description": "lists directory entries from nextcloud webdav. supports shallow or deep listing.",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fpath": {
|
||||
"type": "string",
|
||||
"description": "directory path in nextcloud webdav (e.g., /Documents)"
|
||||
},
|
||||
"deep": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "whether to recurse into subdirectories"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fpath"
|
||||
]
|
||||
},
|
||||
"examples": {
|
||||
"default": {
|
||||
"value": {
|
||||
"fpath": "/Documents",
|
||||
"deep": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "directory listing",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/DirEntry"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/Unauthorized"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/components/responses/NotFound"
|
||||
},
|
||||
"500": {
|
||||
"$ref": "#/components/responses/ServerError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/openapi.json": {
|
||||
"get": {
|
||||
"operationId": "nextcloudGetOpenapi",
|
||||
"summary": "Serve OpenAPI schema",
|
||||
"description": "serves this specification (used by open webui when registering a tool server).",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "openapi document",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"DirEntry": {
|
||||
"type": "object",
|
||||
"description": "entry returned by webdav client",
|
||||
"properties": {
|
||||
"filename": {
|
||||
"type": "string",
|
||||
"example": "/Documents/report.pdf"
|
||||
},
|
||||
"basename": {
|
||||
"type": "string",
|
||||
"example": "report.pdf"
|
||||
},
|
||||
"lastmod": {
|
||||
"type": "string",
|
||||
"example": "Mon, 01 Sep 2025 12:34:56 GMT"
|
||||
},
|
||||
"size": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"example": 2048
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"file",
|
||||
"directory"
|
||||
]
|
||||
},
|
||||
"etag": {
|
||||
"type": "string",
|
||||
"example": "\"a1b2c3d4e5\""
|
||||
},
|
||||
"mime": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"example": "application/pdf"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"basename",
|
||||
"type"
|
||||
]
|
||||
},
|
||||
"FileMeta": {
|
||||
"type": "object",
|
||||
"description": "metadata about a file (json-friendly alternative to raw bytes)",
|
||||
"properties": {
|
||||
"filename": {
|
||||
"type": "string"
|
||||
},
|
||||
"basename": {
|
||||
"type": "string"
|
||||
},
|
||||
"lastmod": {
|
||||
"type": "string"
|
||||
},
|
||||
"size": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"file",
|
||||
"directory"
|
||||
]
|
||||
},
|
||||
"etag": {
|
||||
"type": "string"
|
||||
},
|
||||
"mime": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"cached": {
|
||||
"type": "boolean",
|
||||
"description": "whether the file was served from local cache"
|
||||
},
|
||||
"download_url": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"description": "url to download raw bytes (may be the same endpoint with different accept header)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"basename",
|
||||
"type"
|
||||
]
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"Unauthorized": {
|
||||
"description": "not authorized to access the requested path (middleware denied or missing parameters)",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ok": {
|
||||
"type": "boolean",
|
||||
"example": false
|
||||
},
|
||||
"error": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"NotFound": {
|
||||
"description": "path not found upstream",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ok": {
|
||||
"type": "boolean",
|
||||
"example": false
|
||||
},
|
||||
"error": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ServerError": {
|
||||
"description": "unexpected server error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ok": {
|
||||
"type": "boolean",
|
||||
"example": false
|
||||
},
|
||||
"error": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "nextcloud",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/express": "^5.0.3",
|
||||
"@types/multer": "^2.0.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^5.1.0",
|
||||
"multer": "^2.0.2",
|
||||
"webdav": "^5.8.0"
|
||||
}
|
||||
}
|
||||
@@ -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