Karl I think the point here, is what will this "link" look like, where will it get stored, and how are you going to assign specific settings to a single link? Is it temporary? If so, what mechanism will control this? And this link is public? I would assume so, because I don't see how you are supposed to limit this link to certain other users.
Karl How you intend to achieve that from a physical file system, I do not know ...
Maybe an example will help 🙂
Here is my current Nginx config:
upstream files_gallery {
server unix:/run/php-fpm83/files_gallery.sock;
}
server {
server_name files.local;
set $logname files_gallery;
include includes/logging_access.conf;
error_log /var/log/nginx/files_gallery.error.log;
include includes/defaults.conf;
include includes/authentik/outpost.conf;
# timeout for choked downloads
send_timeout 10m;
# explore directories in the ui
if ($uri ~ ^/(.+)/$) {
return 302 /?$1;
}
# NOTE: files gallery has internal links pointing here
location = /index.php {
# if user has files with internal names in their dir that they're
# trying to open, proxy them through the script instead of serving the
# internal file with nginx
set $x $arg_action#$arg_resize#$arg_file;
if ($x ~ ^file##(?!(?:index|db)\.php$|_files_assets(?:$|/))) {
# use rewrite for uri normalization
rewrite ^ /$arg_file? redirect;
}
root /userfiles/$authentik_username;
fastcgi_pass files_gallery;
include fastcgi.conf;
fastcgi_param SCRIPT_FILENAME /srv/www/files_gallery/index.php;
# tuning upload buffers for http2
# https://blog.cloudflare.com/delivering-http-2-upload-speed-improvements/
client_body_buffer_size 512k;
# set max upload size and increase upload timeout
client_max_body_size 5G;
client_body_timeout 2h;
# more response buffers for better large file perf
fastcgi_buffers 64 8k;
# timeout for choked downloads
fastcgi_read_timeout 10m;
include includes/authentik/location.conf;
}
# open files gallery at root dir
location = / {
rewrite / /index.php last;
}
# custom db accessor
location = /db.php {
root /srv/www/files_gallery;
fastcgi_pass files_gallery;
include fastcgi.conf;
include includes/authentik/location.conf;
}
# serve assets
location /_files_assets/ {
alias /srv/www/files_gallery/assets/;
include includes/authentik/location.conf;
}
# serve user files
location / {
alias /userfiles/$authentik_username/;
error_page 404 @show_file_in_ui;
include includes/authentik/location.conf;
}
# make 404's go back to ui, looking for the specific file
location @show_file_in_ui {
rewrite ^/(.*)/([^/]+)$ /?$1#$2 redirect;
rewrite ^/(.*) /?#$1 redirect;
}
}
# vim:ft=nginx:sts=4:sw=4:et
If you couldn't tell, I use authentik for SSO, and get the username from it.
And the script for shared links (WIP but working):
<?php
require_once __DIR__ . '/_files/lib/vendor/autoload.php';
use Opis\JsonSchema\{
Validator,
Errors\ErrorFormatter,
};
$success = false;
$msg = 'unknown error';
try {
$db = __DIR__ . '/_files/db.sql';
$actions = ['initdb', 'share.create', 'share.get', 'share.auth'];
if (empty($_GET['action']) || !in_array($action = $_GET['action'], $actions)) {
throw new InvalidArgumentException('Parameter "action" must be one of "' .
implode('", "', $actions) . '"');
}
if ($action == 'initdb') {
if (file_exists($db)) {
throw new RuntimeException('database already exists');
}
$db = new SQLite3($db);
$db->enableExceptions(true);
$db->exec('
CREATE TABLE share (
name TEXT,
path TEXT,
owner_uid TEXT,
expiry INTEGER,
perms TEXT
);
CREATE TABLE usersettings (
uid TEXT PRIMARY KEY,
path TEXT,
owner TEXT,
expiry INTEGER,
perms TEXT
);');
$msg = 'created database';
$success = true;
goto end;
}
if (!file_exists($db)) {
throw new RuntimeException('database does not exist, create it with action=initdb');
}
if (str_starts_with($action, 'share.')) {
if ($action == 'share.create') {
if (empty($_GET['name']) || empty($_GET['path'])) {
throw new InvalidArgumentException('Must specify "name" and "path"');
}
$name = $_GET['name'];
$path = $_GET['path'];
$owner = $_SERVER['X_AUTHENTIK_UID'];
$expiry = (int) @$_GET['expiry'];
if ($expiry <= 0) {
$expiry = time() + 60 * 60 * 24 * 7;
}
if (empty($_GET['perms'])) {
$perms = json_decode('[{"type": "authenticated", "value": true}]', false);
} else {
$perms = json_decode($_GET['perms'], false, 4, JSON_THROW_ON_ERROR);
}
$result = (new Validator())->validate($perms, '{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "array",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["type"],
"allOf": [{
"if": {"properties": {"type": {"const": "default"}}},
"then": {"required": ["config"]},
"else": {"required": ["value"]}
}, {
"if": {"properties": {"type": {"const": "authenticated"}}},
"then": {"properties": {"value": {"type": "boolean"}}},
"else": {
"if": {"properties": {"type": {"const": "access_count"}}},
"then": {"properties": {"value": {"type": "integer"}}},
"else": {"properties": {"value": {"type": "string"}}}
}
}],
"properties": {
"type": {
"title": "Access control type",
"enum": ["default", "authenticated", "uid", "email", "password", "access_count", "agree_terms"]
},
"value": {},
"config": {
"type": "object",
"minProperties": 1,
"additionalProperties": false,
"properties": {
"load_images": {
"title": "Show image previews",
"type": "boolean"
},
"video_thumbs": {
"title": "Show video thumbnails",
"type": "boolean"
},
"folder_preview_image": {
"title": "Show folder previews",
"type": "boolean"
},
"menu_enabled": {
"title": "Enable the navigation sidebar",
"type": "boolean"
},
"menu_max_depth": {
"title": "Menu directory depth",
"type": "boolean"
},
"menu_sort": {
"title": "Menu sort order",
"type": "string",
"enum": ["name_asc", "name_desc", "date_asc", "date_desc"]
},
"layout": {
"title": "Layout",
"type": "string",
"enum": ["list", "imagelist", "blocks", "grid", "rows", "columns"]
},
"sort": {
"title": "Sort order",
"type": "string",
"enum": ["name_asc", "name_desc", "date_asc", "date_desc"]
},
"sort_dirs_first": {
"title": "Sort directories first",
"type": "boolean"
},
"files_exclude": {
"title": "Exclude files matching pattern",
"type": "string"
},
"dirs_exclude": {
"title": "Exclude directories matching pattern",
"type": "string"
},
"click": {
"title": "Mouse click action",
"type": "string",
"enum": ["popup", "modal", "download", "window", "menu"]
},
"context_menu": {
"title": "Enable right-click context menu",
"type": "boolean"
},
"prevent_right_click": {
"title": "Block right-click on images",
"type": "boolean"
},
"allow_upload": { "type": "boolean" },
"allow_delete": { "type": "boolean" },
"allow_rename": { "type": "boolean" },
"allow_new_folder": { "type": "boolean" },
"allow_new_file": { "type": "boolean" },
"allow_duplicate": { "type": "boolean" },
"allow_text_edit": { "type": "boolean" },
"allow_zip": { "type": "boolean" },
"allow_unzip": { "type": "boolean" },
"allow_move": { "type": "boolean" },
"allow_copy": { "type": "boolean" },
"allow_download": { "type": "boolean" },
"allow_mass_download": { "type": "boolean" },
"allow_mass_copy_links": { "type": "boolean" },
"upload_allowed_file_types": {
"title": "Allowed file types for upload",
"type": "string"
},
"upload_max_filesize": {
"title": "Max size of uploaded files",
"type": "integer",
"minimum": 0
},
"lang_default": {
"title": "Default interface language",
"type": "string"
}
}
},
"onerror": {
"type": "string"
}
}
}
}');
if (!$result->isValid()) {
throw new InvalidArgumentException('Error validating "perms": ' .
json_encode((new ErrorFormatter)->format($result->error())));
}
$db = new PDO('sqlite:'.$db, options: [
PDO::ATTR_TIMEOUT => 1,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
$db
->prepare('INSERT INTO share VALUES (:name, :path, :owner, :expiry, :perms)')
->execute([':name'=>$name, ':path'=>$path, ':owner'=>$owner,
':expiry'=>$expiry, ':perms'=>json_encode($perms)]);
$msg = ['id' => $db->lastInsertId()];
$success = true;
goto end;
}
}
http_response_code(502);
$msg = 'Not implemented';
} catch (Throwable $e) {
$msg = $e->__toString();
while($e = $e->getPrevious()) {
$msg .= "\nPrevious exception: " . $e->__toString();
}
$msg = str_replace(__DIR__, '', $msg);
}
end:
header('Content-Type: application/json');
if (is_array($msg)) {
$msg['success'] = $success;
echo json_encode($msg, JSON_THROW_ON_ERROR, 2);
} else {
echo json_encode(['success'=>$success, 'msg'=>$msg]);
}
// vim:et:sts=2:sw=2
?>
Currently I only have the database and adding new entries done. For example, to add the shared link https://files.local/s/MyShareName, and have it show the files in the directory somepath
inside my files, and to only allow authenticated users to be able to access the link, you might do:
> GET https://files.local/db.php?action=share.create&name=MyShareName&path=/sev/somepath&perms=[{%22type%22:%20%22authenticated%22,%20%22value%22:%20true}]
< {"id":"4","success":true}
I think it's pretty straightforward.
You may also notice the usersettings
table; I do plan to also add persistent per-user settings. If your implementation ends up being better I will look at supplanting it.
I also wrote a JSON validator to ensure correctness before writing to the database. I am planning to use the hinting in the schema to build the UI programmatically, if that ends up being feasible.
The config is basically done for now, though I have not yet added routing for the shared link. Routing from Nginx will be pretty simple:
- Use
auth_request /db.php?action=share.auth&name=$1
to send a subrequest to authenticate the user accessing the share
db.php
will look up the share to see if it exists, and validate the requested permissions
- If any permissions require user interaction, redirect to
db.php?action=share.validate
(real link TBD)
- If passing (either original subrequest or redirect), start a PHP session to store the success and share path, and redirect back to original page with a header containing the share path
- Use e.g.
auth_request_set $files_gallery_share_root $upstream_http_x_files_gallery_share_root
to pull in the share path from the subrequest; can pull it from the PHP session if already authenticated, to avoid multiple DB lookups for successive requests
The (not) fun part is going to be the JavaScript for the UI, because I'm not really a fan of writing it 🙂
Anyway, I do still disagree on calling this a virtualization of the filesystem. All I am doing is changing the root. I am not in any way interfering with how Files Gallery works, if anything I am merely extrapolating on its strengths. I wonder if this example might help you understand what it is I am trying to do.