commit 617276b285cf7f7aa5ae76829085bf7602f9cfdd Author: Matt Date: Sat May 16 03:07:37 2026 -0400 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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..6f3d3b9 --- /dev/null +++ b/README.md @@ -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 +``` diff --git a/etc/config/copyparty b/etc/config/copyparty new file mode 100644 index 0000000..7546162 --- /dev/null +++ b/etc/config/copyparty @@ -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' diff --git a/etc/init.d/copyparty b/etc/init.d/copyparty new file mode 100755 index 0000000..7f64b09 --- /dev/null +++ b/etc/init.d/copyparty @@ -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" +} diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..f35a2e3 --- /dev/null +++ b/install.sh @@ -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" diff --git a/usr/share/luci/menu.d/luci-app-copyparty.json b/usr/share/luci/menu.d/luci-app-copyparty.json new file mode 100644 index 0000000..2f76678 --- /dev/null +++ b/usr/share/luci/menu.d/luci-app-copyparty.json @@ -0,0 +1,14 @@ +{ + "admin/services/copyparty": { + "title": "Copyparty", + "order": 70, + "action": { + "type": "view", + "path": "copyparty" + }, + "depends": { + "acl": [ "luci-app-copyparty" ], + "uci": { "copyparty": true } + } + } +} diff --git a/usr/share/rpcd/acl.d/luci-app-copyparty.json b/usr/share/rpcd/acl.d/luci-app-copyparty.json new file mode 100644 index 0000000..9e2ff33 --- /dev/null +++ b/usr/share/rpcd/acl.d/luci-app-copyparty.json @@ -0,0 +1,11 @@ +{ + "luci-app-copyparty": { + "description": "Grant UCI access for luci-app-copyparty", + "read": { + "uci": [ "copyparty" ] + }, + "write": { + "uci": [ "copyparty" ] + } + } +} diff --git a/www/luci-static/resources/view/copyparty.js b/www/luci-static/resources/view/copyparty.js new file mode 100644 index 0000000..684c70c --- /dev/null +++ b/www/luci-static/resources/view/copyparty.js @@ -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 port 3923.').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; + }); + }); + } +});