mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 10:37:07 +00:00
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:
@@ -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)': {
|
||||
|
||||
0
lib/www/server/api/middleware/files/delete.js
Normal file
0
lib/www/server/api/middleware/files/delete.js
Normal file
29
lib/www/server/api/middleware/files/get.js
Normal file
29
lib/www/server/api/middleware/files/get.js
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
7
lib/www/server/api/middleware/files/index.js
Normal file
7
lib/www/server/api/middleware/files/index.js
Normal file
@@ -0,0 +1,7 @@
|
||||
|
||||
module.exports = {
|
||||
get: require('./get'),
|
||||
post: require('./post'),
|
||||
put: require('./put'),
|
||||
delete: require('./delete')
|
||||
}
|
||||
0
lib/www/server/api/middleware/files/post.js
Normal file
0
lib/www/server/api/middleware/files/post.js
Normal file
0
lib/www/server/api/middleware/files/put.js
Normal file
0
lib/www/server/api/middleware/files/put.js
Normal file
@@ -1,5 +1,6 @@
|
||||
module.exports = {
|
||||
event: require('./event'),
|
||||
files: require('./files'),
|
||||
plan: require('./plan'),
|
||||
line: require('./line'),
|
||||
project: require('./project'),
|
||||
|
||||
0
lib/www/server/lib/files/delete.js
Normal file
0
lib/www/server/lib/files/delete.js
Normal file
125
lib/www/server/lib/files/get.js
Normal file
125
lib/www/server/lib/files/get.js
Normal 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"};
|
||||
}
|
||||
}
|
||||
}*/
|
||||
7
lib/www/server/lib/files/index.js
Normal file
7
lib/www/server/lib/files/index.js
Normal file
@@ -0,0 +1,7 @@
|
||||
|
||||
module.exports = {
|
||||
get: require('./get'),
|
||||
post: require('./post'),
|
||||
put: require('./put'),
|
||||
delete: require('./delete')
|
||||
}
|
||||
71
lib/www/server/lib/files/logical.js
Normal file
71
lib/www/server/lib/files/logical.js
Normal 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
|
||||
};
|
||||
22
lib/www/server/lib/files/mime-types.js
Normal file
22
lib/www/server/lib/files/mime-types.js
Normal 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;
|
||||
0
lib/www/server/lib/files/post.js
Normal file
0
lib/www/server/lib/files/post.js
Normal file
0
lib/www/server/lib/files/put.js
Normal file
0
lib/www/server/lib/files/put.js
Normal file
Reference in New Issue
Block a user