Initial release: procd service and LuCI app for copyparty
OpenWrt procd service script and LuCI JavaScript interface for copyparty (https://github.com/9001/copyparty), a self-hosted file sharing server. Features: - procd service with UCI-driven volumes, accounts, and TLS cert - LuCI view: settings, volume/account grids, live status + start/stop - ACME TLS support via combined key+fullchain PEM (--cert flag) - Auto-respawn and reload-on-config-change via service_triggers
This commit is contained in:
80
README.md
Normal file
80
README.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# luci-app-copyparty
|
||||||
|
|
||||||
|
OpenWrt procd service and LuCI interface for [copyparty](https://github.com/9001/copyparty), a self-hosted file sharing server.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- OpenWrt 23.05 or later (uses LuCI JavaScript views)
|
||||||
|
- `python3` package installed
|
||||||
|
- copyparty single-file bundle (e.g. `copyparty-en.py`) downloaded separately from the [copyparty releases page](https://github.com/9001/copyparty/releases)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# On the OpenWrt device:
|
||||||
|
sh install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Or copy files manually to their destinations (see `install.sh` for the mapping).
|
||||||
|
|
||||||
|
The UCI config file (`/etc/config/copyparty`) is **not overwritten** if it already exists, so re-running install is safe.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Edit `/etc/config/copyparty`:
|
||||||
|
|
||||||
|
```
|
||||||
|
config copyparty 'config'
|
||||||
|
option enabled '1'
|
||||||
|
option port '3923'
|
||||||
|
option name 'MyNAS'
|
||||||
|
option script '/path/to/copyparty-en.py'
|
||||||
|
option usernames '0' # set to 1 to require username+password
|
||||||
|
|
||||||
|
config volume
|
||||||
|
option src '/mnt/raid/files'
|
||||||
|
option dst '/files'
|
||||||
|
option flags 'r' # r=read, w=write, m=move, d=delete, g=get
|
||||||
|
|
||||||
|
config account
|
||||||
|
option user 'alice'
|
||||||
|
option pass 'secret'
|
||||||
|
```
|
||||||
|
|
||||||
|
Or configure via **LuCI → Services → Copyparty**.
|
||||||
|
|
||||||
|
## Volume flags
|
||||||
|
|
||||||
|
| Flag | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| `r` | Read / browse (anonymous) |
|
||||||
|
| `w` | Write / upload |
|
||||||
|
| `m` | Move / rename |
|
||||||
|
| `d` | Delete |
|
||||||
|
| `g` | Download as file (bypasses browser preview) |
|
||||||
|
|
||||||
|
Common combinations: `r` (read-only), `rw` (read/write), `rwmd` (full access).
|
||||||
|
|
||||||
|
See the [copyparty docs](https://github.com/9001/copyparty#accounts-and-volumes) for the full flag reference.
|
||||||
|
|
||||||
|
## Service management
|
||||||
|
|
||||||
|
```sh
|
||||||
|
/etc/init.d/copyparty enable # start on boot
|
||||||
|
/etc/init.d/copyparty start
|
||||||
|
/etc/init.d/copyparty stop
|
||||||
|
/etc/init.d/copyparty restart
|
||||||
|
```
|
||||||
|
|
||||||
|
## File layout
|
||||||
|
|
||||||
|
```
|
||||||
|
etc/
|
||||||
|
config/copyparty UCI config
|
||||||
|
init.d/copyparty procd service script
|
||||||
|
usr/share/
|
||||||
|
luci/menu.d/luci-app-copyparty.json LuCI menu entry
|
||||||
|
rpcd/acl.d/luci-app-copyparty.json rpcd ACL
|
||||||
|
www/luci-static/resources/view/
|
||||||
|
copyparty.js LuCI JavaScript view
|
||||||
|
```
|
||||||
17
etc/config/copyparty
Normal file
17
etc/config/copyparty
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
config copyparty 'config'
|
||||||
|
option enabled '0'
|
||||||
|
option port '3923'
|
||||||
|
option name 'OWRT-NAS'
|
||||||
|
option script '/mnt/raid/copyparty-en.py'
|
||||||
|
option usernames '0'
|
||||||
|
|
||||||
|
# Example volume:
|
||||||
|
# config volume
|
||||||
|
# option src '/mnt/raid/files'
|
||||||
|
# option dst '/files'
|
||||||
|
# option flags 'r'
|
||||||
|
|
||||||
|
# Example account:
|
||||||
|
# config account
|
||||||
|
# option user 'alice'
|
||||||
|
# option pass 'secret'
|
||||||
57
etc/init.d/copyparty
Executable file
57
etc/init.d/copyparty
Executable file
@@ -0,0 +1,57 @@
|
|||||||
|
#!/bin/sh /etc/rc.common
|
||||||
|
|
||||||
|
START=99
|
||||||
|
USE_PROCD=1
|
||||||
|
NAME=copyparty
|
||||||
|
PROG=/usr/bin/python3
|
||||||
|
|
||||||
|
_add_volume() {
|
||||||
|
local cfg="$1"
|
||||||
|
local src dst flags
|
||||||
|
config_get src "$cfg" src
|
||||||
|
config_get dst "$cfg" dst
|
||||||
|
config_get flags "$cfg" flags 'r'
|
||||||
|
[ -n "$src" ] && [ -n "$dst" ] && \
|
||||||
|
procd_append_param command -v "${src}:${dst}:${flags}"
|
||||||
|
}
|
||||||
|
|
||||||
|
_add_account() {
|
||||||
|
local cfg="$1"
|
||||||
|
local user pass
|
||||||
|
config_get user "$cfg" user
|
||||||
|
config_get pass "$cfg" pass
|
||||||
|
[ -n "$user" ] && [ -n "$pass" ] && \
|
||||||
|
procd_append_param command -a "${user}:${pass}"
|
||||||
|
}
|
||||||
|
|
||||||
|
start_service() {
|
||||||
|
config_load "$NAME"
|
||||||
|
|
||||||
|
local enabled
|
||||||
|
config_get_bool enabled config enabled 0
|
||||||
|
[ "$enabled" = "1" ] || return 0
|
||||||
|
|
||||||
|
local port name script usernames tls_cert
|
||||||
|
config_get port config port 3923
|
||||||
|
config_get name config name 'OWRT-NAS'
|
||||||
|
config_get script config script '/mnt/raid/copyparty-en.py'
|
||||||
|
config_get_bool usernames config usernames 0
|
||||||
|
config_get tls_cert config tls_cert ''
|
||||||
|
|
||||||
|
procd_open_instance
|
||||||
|
procd_set_param command "$PROG" "$script" -p "$port" --name "$name"
|
||||||
|
[ "$usernames" = "1" ] && procd_append_param command --usernames
|
||||||
|
[ -n "$tls_cert" ] && procd_append_param command --cert "$tls_cert"
|
||||||
|
|
||||||
|
config_foreach _add_volume volume
|
||||||
|
config_foreach _add_account account
|
||||||
|
|
||||||
|
procd_set_param respawn
|
||||||
|
procd_set_param stdout 1
|
||||||
|
procd_set_param stderr 1
|
||||||
|
procd_close_instance
|
||||||
|
}
|
||||||
|
|
||||||
|
service_triggers() {
|
||||||
|
procd_add_reload_trigger "$NAME"
|
||||||
|
}
|
||||||
26
install.sh
Normal file
26
install.sh
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Install luci-app-copyparty onto an OpenWrt device.
|
||||||
|
# Run on the device itself, or adapt for scp.
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
DEST_INIT=/etc/init.d/copyparty
|
||||||
|
DEST_UCI=/etc/config/copyparty
|
||||||
|
DEST_VIEW=/www/luci-static/resources/view/copyparty.js
|
||||||
|
DEST_MENU=/usr/share/luci/menu.d/luci-app-copyparty.json
|
||||||
|
DEST_ACL=/usr/share/rpcd/acl.d/luci-app-copyparty.json
|
||||||
|
|
||||||
|
SCRIPT_DIR=$(dirname "$0")
|
||||||
|
|
||||||
|
cp "$SCRIPT_DIR/etc/init.d/copyparty" "$DEST_INIT"
|
||||||
|
chmod +x "$DEST_INIT"
|
||||||
|
|
||||||
|
[ -f "$DEST_UCI" ] || cp "$SCRIPT_DIR/etc/config/copyparty" "$DEST_UCI"
|
||||||
|
|
||||||
|
cp "$SCRIPT_DIR/www/luci-static/resources/view/copyparty.js" "$DEST_VIEW"
|
||||||
|
cp "$SCRIPT_DIR/usr/share/luci/menu.d/luci-app-copyparty.json" "$DEST_MENU"
|
||||||
|
cp "$SCRIPT_DIR/usr/share/rpcd/acl.d/luci-app-copyparty.json" "$DEST_ACL"
|
||||||
|
|
||||||
|
/etc/init.d/rpcd restart
|
||||||
|
|
||||||
|
echo "Installed. Edit $DEST_UCI then: /etc/init.d/copyparty enable && /etc/init.d/copyparty start"
|
||||||
14
usr/share/luci/menu.d/luci-app-copyparty.json
Normal file
14
usr/share/luci/menu.d/luci-app-copyparty.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"admin/services/copyparty": {
|
||||||
|
"title": "Copyparty",
|
||||||
|
"order": 70,
|
||||||
|
"action": {
|
||||||
|
"type": "view",
|
||||||
|
"path": "copyparty"
|
||||||
|
},
|
||||||
|
"depends": {
|
||||||
|
"acl": [ "luci-app-copyparty" ],
|
||||||
|
"uci": { "copyparty": true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
usr/share/rpcd/acl.d/luci-app-copyparty.json
Normal file
11
usr/share/rpcd/acl.d/luci-app-copyparty.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"luci-app-copyparty": {
|
||||||
|
"description": "Grant UCI access for luci-app-copyparty",
|
||||||
|
"read": {
|
||||||
|
"uci": [ "copyparty" ]
|
||||||
|
},
|
||||||
|
"write": {
|
||||||
|
"uci": [ "copyparty" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
134
www/luci-static/resources/view/copyparty.js
Normal file
134
www/luci-static/resources/view/copyparty.js
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
'use strict';
|
||||||
|
'require form';
|
||||||
|
'require rpc';
|
||||||
|
'require ui';
|
||||||
|
'require view';
|
||||||
|
|
||||||
|
var callInitAction = rpc.declare({
|
||||||
|
object: 'rc',
|
||||||
|
method: 'init',
|
||||||
|
params: ['name', 'action'],
|
||||||
|
expect: { result: false }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callServiceList = rpc.declare({
|
||||||
|
object: 'service',
|
||||||
|
method: 'list',
|
||||||
|
params: ['name'],
|
||||||
|
expect: { '': {} }
|
||||||
|
});
|
||||||
|
|
||||||
|
return view.extend({
|
||||||
|
handleStart: function() {
|
||||||
|
return callInitAction('copyparty', 'start').then(function() {
|
||||||
|
return window.location.reload();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleStop: function() {
|
||||||
|
return callInitAction('copyparty', 'stop').then(function() {
|
||||||
|
return window.location.reload();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
return callServiceList('copyparty').then(function(res) {
|
||||||
|
var instances = ((res.copyparty || {}).instances) || {};
|
||||||
|
var running = Object.keys(instances).some(function(k) {
|
||||||
|
return instances[k].running;
|
||||||
|
});
|
||||||
|
|
||||||
|
var m, s, o;
|
||||||
|
|
||||||
|
m = new form.Map('copyparty', _('Copyparty'),
|
||||||
|
_('Self-hosted file sharing server. Web UI at <a href="https://%s:3923" target="_blank">port 3923</a>.').format(window.location.hostname));
|
||||||
|
|
||||||
|
/* ── Global settings ── */
|
||||||
|
s = m.section(form.NamedSection, 'config', 'copyparty', _('Settings'));
|
||||||
|
s.addremove = false;
|
||||||
|
|
||||||
|
o = s.option(form.Flag, 'enabled', _('Enable'));
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'port', _('Port'));
|
||||||
|
o.datatype = 'port';
|
||||||
|
o.default = '3923';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'name', _('Server name'));
|
||||||
|
o.default = 'OWRT-NAS';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'script', _('Script path'));
|
||||||
|
o.default = '/mnt/raid/copyparty-en.py';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = s.option(form.Flag, 'usernames', _('Require usernames'),
|
||||||
|
_('If enabled, clients must supply a username in addition to password.'));
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'tls_cert', _('TLS certificate'),
|
||||||
|
_('Path to a PEM file containing the private key followed by the full certificate chain. Leave empty for HTTP.'));
|
||||||
|
o.placeholder = '/etc/acme/owrt-nas.lan.1qaz.ca_ecc/copyparty.pem';
|
||||||
|
o.rmempty = true;
|
||||||
|
|
||||||
|
/* ── Volumes ── */
|
||||||
|
s = m.section(form.GridSection, 'volume', _('Volumes'),
|
||||||
|
_('Each volume maps a filesystem path to a URL path. Flags: r=read, w=write, m=move, d=delete, g=get.'));
|
||||||
|
s.addremove = true;
|
||||||
|
s.anonymous = true;
|
||||||
|
s.addbtntitle = _('Add volume...');
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'src', _('Source path'));
|
||||||
|
o.rmempty = false;
|
||||||
|
o.placeholder = '/mnt/raid/firstshare';
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'dst', _('URL path'));
|
||||||
|
o.rmempty = false;
|
||||||
|
o.placeholder = '/files';
|
||||||
|
|
||||||
|
o = s.option(form.ListValue, 'flags', _('Access'));
|
||||||
|
o.value('r', _('Read-only'));
|
||||||
|
o.value('rw', _('Read/write'));
|
||||||
|
o.value('rwmd', _('Read/write/move/delete'));
|
||||||
|
o.default = 'r';
|
||||||
|
|
||||||
|
/* ── Accounts ── */
|
||||||
|
s = m.section(form.GridSection, 'account', _('Accounts'),
|
||||||
|
_('Leave empty to allow anonymous access. Passwords are stored in plaintext in UCI.'));
|
||||||
|
s.addremove = true;
|
||||||
|
s.anonymous = true;
|
||||||
|
s.addbtntitle = _('Add account...');
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'user', _('Username'));
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'pass', _('Password'));
|
||||||
|
o.rmempty = false;
|
||||||
|
o.password = true;
|
||||||
|
|
||||||
|
return m.render().then(function(node) {
|
||||||
|
var statusBadge = E('span', {
|
||||||
|
style: 'font-weight:bold; color:' + (running ? '#2bab2b' : '#cc0000')
|
||||||
|
}, running ? _('Running') : _('Stopped'));
|
||||||
|
|
||||||
|
var btn = E('button', {
|
||||||
|
class: 'cbi-button cbi-button-' + (running ? 'negative' : 'apply'),
|
||||||
|
click: ui.createHandlerFn(self, running ? 'handleStop' : 'handleStart')
|
||||||
|
}, running ? _('Stop') : _('Start'));
|
||||||
|
|
||||||
|
var statusDiv = E('div', { class: 'cbi-section' }, [
|
||||||
|
E('h3', {}, _('Status')),
|
||||||
|
E('div', { class: 'cbi-section-node' }, [
|
||||||
|
E('p', { style: 'margin:0' }, [ statusBadge, ' ', btn ])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
|
||||||
|
node.insertBefore(statusDiv, node.querySelector('.cbi-section'));
|
||||||
|
return node;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user