Add API files endpoint.

Used to download files. It relies on `imports.paths` being set
appropriately in `etc/config.yaml` to indicate which parts of
the filesystem are accessible to users via Dougal.
This commit is contained in:
D. Berge
2023-08-30 13:51:31 +02:00
parent ec03627119
commit 70cf59bb4c
14 changed files with 268 additions and 0 deletions

View File

@@ -252,6 +252,12 @@ app.map({
// // post: [ mw.permissions.post ],
// // delete: [ mw.permissions.delete ]
// },
'/project/:project/files/:path(*)': {
get: [ mw.auth.access.write, mw.files.get ]
},
'/files/?:path(*)': {
get: [ mw.auth.access.write, mw.files.get ]
},
'/navdata/': {
get: [ mw.navdata.get ],
'gis/:featuretype(line|point)': {

View File

@@ -0,0 +1,29 @@
const files = require('../../../lib/files');
module.exports = async function (req, res, next) {
try {
const entity = await files.get(req.params.path, req.params.project, req.query);
if (entity) {
if (entity.download) {
res.download(...entity.download, (err) => next(err));
} else {
// Directory listing
res.status(203).json(entity);
next();
}
} else {
throw {
status: 404,
code: "ENOENT"
};
}
} catch (err) {
if (err.code == 'ENOENT') {
res.status(404).json({message: err.code});
} else {
next(err);
}
}
};

View File

@@ -0,0 +1,7 @@
module.exports = {
get: require('./get'),
post: require('./post'),
put: require('./put'),
delete: require('./delete')
}

View File

@@ -1,5 +1,6 @@
module.exports = {
event: require('./event'),
files: require('./files'),
plan: require('./plan'),
line: require('./line'),
project: require('./project'),

View File

View File

@@ -0,0 +1,125 @@
const fs = require('fs/promises');
const Path = require('path');
const mime = require('./mime-types');
const { translatePath, logicalRoot } = require('./logical');
const systemCfg = require('../config');
const projectCfg = require('../db/configuration');
async function directoryListing (fullPath, root) {
const contents = await fs.readdir(fullPath, {withFileTypes: true});
const listing = [];
for (const entry of contents) {
const resolved = Path.resolve(fullPath, entry.name);
const relative = resolved.substring(fullPath.length).replace(/^\/+/, "");
const logical = Path.join(root, relative);
const stat = await fs.stat(resolved);
const mimetype = entry.isDirectory()
? "inode/directory"
: (mime.contentType(entry.name) || "application/octet-stream");
listing.push({
path: logical,
basename: Path.basename(relative),
"Content-Type": mimetype,
size: stat.size,
atime: stat.atime,
mtime: stat.mtime,
ctime: stat.ctime,
birthtime: stat.birthtime
});
}
return listing;
}
async function virtualDirectoryListing (logicalPaths) {
const listing = [];
for (const logical of logicalPaths) {
const fullPath = translatePath(logical);
const resolved = Path.resolve("/", logical);
const stat = await fs.stat(fullPath);
const mimetype = stat.isDirectory()
? "inode/directory"
: (mime.contentType(fullPath) || "application/octet-stream");
listing.push({
path: resolved,
basename: Path.basename(logical),
"Content-Type": mimetype,
size: stat.size,
atime: stat.atime,
mtime: stat.mtime,
ctime: stat.ctime,
birthtime: stat.birthtime
});
}
return listing;
}
async function projectRelativeGet (path, query) {
console.log("Not implemented yet");
throw {status: 404, message: "ENOENT"};
}
async function systemRelativeGet (path, query) {
try {
if (!path) {
return await systemRelativeGet("/", query);
} else if (Path.resolve(path) == "/") {
return await virtualDirectoryListing(logicalRoot())
} else {
const physicalPath = translatePath(path);
const stats = await fs.stat(physicalPath);
if (stats.isDirectory()) {
// Return directory listing, with types.
return await directoryListing(physicalPath, "/"+path.replace(/^\/+/, ""));
} else if (stats.isFile()) {
// Returns a list of arguments suitable for ExpressJS res.download
const headers = {
"Content-Type": mime.lookup(physicalPath) || "application/octet-stream"
};
return {
download: [ physicalPath, Path.basename(path), { headers } ]
};
} else {
throw {status: 403, message: "ENOACCESS"};
}
}
} catch (err) {
console.error(err);
throw err;
}
}
module.exports = async function get (path, projectId, query) {
if (projectId) {
return await projectRelativeGet(path, query);
} else {
return await systemRelativeGet(path, query);
}
};
/*
module.exports = async function get (path, projectId, query) {
const root = projectId
? Path.resolve(systemCfg.global.files.root, await projectCfg.get(projectId, "rootPath"))
: systemCfg.global.files.root;
const fullPath = Path.resolve(root, path);
// Check if there is an attempt to break out of root path
if (Path.relative(root, fullPath).includes("..")) {
// Throw something resolving to a 404
throw {status: 404, message: "ENOENT"};
} else {
const stats = await fs.stat(fullPath);
if (stats.isDirectory()) {
// Return directory listing, with types.
return await directoryListing(fullPath, root);
} else if (stats.isFile()) {
// Returns a list of arguments suitable for ExpressJS res.download
return {
download: [ fullPath, Path.basename(path) ]
};
} else {
throw {status: 403, message: "ENOACCESS"};
}
}
}*/

View File

@@ -0,0 +1,7 @@
module.exports = {
get: require('./get'),
post: require('./post'),
put: require('./put'),
delete: require('./delete')
}

View File

@@ -0,0 +1,71 @@
const Path = require('path');
const cfg = require('../config');
function translatePath (file) {
const root = Path.resolve(cfg.DOUGAL_ROOT);
const importPaths = cfg.global?.imports?.paths;
function validate (physicalPath, prefix) {
if (physicalPath.startsWith(prefix)) {
return physicalPath;
} else {
// An attempt to break out of the logical path?
throw {
status: 404,
message: "Not found"
};
}
}
if (Path.isAbsolute(file)) {
if (typeof importPaths === "string") {
// Substitute the root for the real physical path
// NOTE: `root` deals with import_paths not being absolute
const prefix = Path.resolve(Path.join(root, importPaths));
const suffix = Path.resolve(file).replace(/^\/+/, "");
const physicalPath = Path.resolve(Path.join(prefix, suffix));
return validate(physicalPath, prefix);
} else if (typeof importPaths === "object") {
const parts = Path.resolve(file).split("/").slice(1);
if (parts[0] in importPaths) {
const prefix = Path.join("/", importPaths[parts[0]])
const suffix = parts.slice(1).join("/");
const physicalPath = Path.resolve(Path.join(prefix, suffix));
return validate(physicalPath, prefix);
} else {
return validate(file, null); // Throws 404
}
} else {
// Most likely importPaths is undefined
return validate(file, null); // Throws 404
}
} else {
// A relative filepath is always resolved relative to the logical root
return translatePath(Path.resolve(Path.join("/", file)));
}
}
function untranslatePath (file) {
}
function logicalRoot () {
const root = Path.resolve(cfg.DOUGAL_ROOT);
const importPaths = cfg.global?.imports?.paths;
if (typeof importPaths === "string") {
return [ "/" ];
} else if (typeof importPaths === "object") {
return Object.keys(importPaths);
} else {
// Most likely importPaths is undefined
return [];
}
}
module.exports = {
translatePath,
untranslatePath,
logicalRoot
};

View File

@@ -0,0 +1,22 @@
const mime = require('mime-types');
const extraTypes = {
"text/plain": [
"sps", "SPS",
"p1", "P1",
"p190", "P190",
"p111", "P111",
"p2", "P2",
"p294", "P294",
"p211", "P211",
"hdr", "HDR"
]
};
for (let [mimeType, extensions] of Object.entries(extraTypes)) {
for (let extension of extensions) {
mime.types[extension] = mimeType;
}
}
module.exports = mime;

View File

View File