mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-01 06:13:56 +08:00
This commit is contained in:
1 parent
d9f7c5c117
commit
26d174538f
57 files changed
+2774
-6425
No files matched your search
@@ -5,8 +5,11 @@
|
||||
'require poll';
|
||||
'require rpc';
|
||||
'require uci';
|
||||
'require ui';
|
||||
'require view';
|
||||
|
||||
const HELPER = '/usr/libexec/openlist2-helper';
|
||||
|
||||
const callServiceList = rpc.declare({
|
||||
object: 'service',
|
||||
method: 'list',
|
||||
@@ -25,25 +28,25 @@ const getServiceStatus = () => {
|
||||
};
|
||||
|
||||
const getWebInterfaceUrl = (protocol, webport, site_url) => {
|
||||
const fallbackUrl = `${protocol}//${window.location.hostname}:${webport}/`;
|
||||
const configuredUrl = (site_url || '').trim();
|
||||
if (configuredUrl) {
|
||||
try {
|
||||
const url = new URL(configuredUrl, fallbackUrl);
|
||||
const url = new URL(configuredUrl);
|
||||
if (url.protocol === 'http:' || url.protocol === 'https:')
|
||||
return url.href;
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
return fallbackUrl;
|
||||
return webport ? protocol + '//' + window.location.hostname + ':' + webport + '/' : null;
|
||||
};
|
||||
|
||||
const renderStatus = (isRunning, protocol, webport, site_url) => {
|
||||
const status = E('em', {}, E('span', { 'style': `color:${isRunning ? 'green' : 'red'}` },
|
||||
E('strong', {}, ['OpenList ', isRunning ? _('RUNNING') : _('NOT RUNNING')])));
|
||||
|
||||
if (isRunning && webport) {
|
||||
if (isRunning && (webport || site_url)) {
|
||||
const buttonUrl = getWebInterfaceUrl(protocol, webport, site_url);
|
||||
if (!buttonUrl)
|
||||
return status;
|
||||
const button = E('input', {
|
||||
'class': 'cbi-button-reload',
|
||||
'type': 'button',
|
||||
@@ -58,32 +61,24 @@ const renderStatus = (isRunning, protocol, webport, site_url) => {
|
||||
return status;
|
||||
};
|
||||
|
||||
const isSafeLogPath = value => /^\/var\/log\/openlist2(?:[._-][A-Za-z0-9._-]+)?$/.test(value || '');
|
||||
const isSafeLogPath = value => !/[\r\n]/.test(value || '') &&
|
||||
/^\/var\/log\/openlist2(?:[._-][A-Za-z0-9._-]+)?$/.test(value || '');
|
||||
|
||||
const isEnabledPort = value => {
|
||||
const port = (value || '').trim();
|
||||
return port !== '' && port !== '-1';
|
||||
};
|
||||
|
||||
const HTTPS_ENABLED_PORT = /^[1-9][0-9]*$/;
|
||||
const isEnabledPort = value => /^\d+$/.test(String(value || '').trim()) && Number(value) >= 1 && Number(value) <= 65535;
|
||||
|
||||
const getListenPortValue = (section_id, option, fallback) => {
|
||||
const value = uci.get('openlist2', section_id, option);
|
||||
return value == null || String(value).trim() === '' ? fallback : value;
|
||||
};
|
||||
|
||||
const getHttpsPortValue = (section_id, httpsPortOption) => {
|
||||
const value = httpsPortOption.formvalue(section_id);
|
||||
return value == null || String(value).trim() === ''
|
||||
? getListenPortValue(section_id, 'listen_https_port', '-1')
|
||||
: value;
|
||||
};
|
||||
|
||||
const validateHttpsRequiredFile = (section_id, httpsPortOption, value, message) => {
|
||||
if (!HTTPS_ENABLED_PORT.test(String(getHttpsPortValue(section_id, httpsPortOption)).trim()))
|
||||
return true;
|
||||
|
||||
return (value || '').trim() ? true : message;
|
||||
if (value != null && String(value).trim() !== '')
|
||||
return value;
|
||||
const legacyPort = uci.get('openlist2', section_id, 'port');
|
||||
const legacySsl = uci.get('openlist2', section_id, 'ssl');
|
||||
if (legacyPort != null || legacySsl != null) {
|
||||
const tls = ['1', 'true', 'on', 'yes', 'enabled'].includes(legacySsl);
|
||||
return option === 'listen_https_port'
|
||||
? (tls ? legacyPort || '5244' : '-1')
|
||||
: (tls ? '-1' : legacyPort || '5244');
|
||||
}
|
||||
return fallback;
|
||||
};
|
||||
|
||||
const validateListenPort = value => {
|
||||
@@ -136,49 +131,143 @@ const validateCorsOrigins = value => {
|
||||
};
|
||||
|
||||
const getEffectiveWebEndpoint = config => {
|
||||
const httpPort = uci.get(config, '@openlist2[0]', 'listen_http_port') || '5244';
|
||||
const httpsPort = uci.get(config, '@openlist2[0]', 'listen_https_port') || '-1';
|
||||
const httpPort = getListenPortValue('@openlist2[0]', 'listen_http_port', '5244');
|
||||
const httpsPort = getListenPortValue('@openlist2[0]', 'listen_https_port', '-1');
|
||||
|
||||
if (isEnabledPort(httpsPort))
|
||||
return { protocol: 'https:', port: httpsPort };
|
||||
return { protocol: 'https:', port: String(Number(httpsPort)) };
|
||||
|
||||
if (isEnabledPort(httpPort))
|
||||
return { protocol: 'http:', port: httpPort };
|
||||
return { protocol: 'http:', port: String(Number(httpPort)) };
|
||||
|
||||
return { protocol: 'http:', port: '' };
|
||||
};
|
||||
|
||||
|
||||
const normalizePath = value => String(value || '').replace(/\/+/g, '/').replace(/\/$/, '') || '/';
|
||||
const containsPath = (parent, child) => child === parent || child.startsWith(parent + '/');
|
||||
const broadDirectories = new Set(['/', '/bin', '/boot', '/dev', '/etc', '/home', '/lib', '/lib64', '/media', '/mnt', '/opt', '/overlay', '/proc', '/root', '/rom', '/run', '/sbin', '/srv', '/sys', '/tmp', '/usr', '/var', '/www', '/tmp/log', '/tmp/run']);
|
||||
const isBroadDirectory = path => broadDirectories.has(path) ||
|
||||
/^\/(?:usr|bin|sbin|lib|lib64|dev|proc|sys|rom|overlay|www)\//.test(path) ||
|
||||
/^\/(?:etc\/(?:config|dropbear|ssl)|tmp\/log)(?:\/|$)/.test(path);
|
||||
|
||||
const validateAbsolutePath = (value, required) => {
|
||||
const path = String(value || '');
|
||||
if (!path && !required)
|
||||
return true;
|
||||
if (!path.startsWith('/') || /[\x00-\x1f\x7f]/.test(path) || /\/\.{1,2}(?:\/|$)/.test(path))
|
||||
return _('Use an absolute path without control characters or dot segments.');
|
||||
return true;
|
||||
};
|
||||
|
||||
const passiveRanges = value => (Array.isArray(value) ? value : [value || ''])
|
||||
.flatMap(item => String(item).split(/[,\s]+/)).filter(Boolean);
|
||||
|
||||
const validatePassivePorts = value => {
|
||||
for (const range of passiveRanges(value)) {
|
||||
if (!/^\d+(?:-\d+)?$/.test(range))
|
||||
return _('Use passive ports or ranges from 1024 to 65535.');
|
||||
const [first, last = first] = range.split('-').map(Number);
|
||||
if (first < 1024 || last > 65535 || first > last)
|
||||
return _('Use passive ports or ranges from 1024 to 65535.');
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const backendUrl = route => {
|
||||
const endpoint = getEffectiveWebEndpoint('openlist2');
|
||||
const base = getWebInterfaceUrl(endpoint.protocol, endpoint.port,
|
||||
uci.get('openlist2', '@openlist2[0]', 'site_url'));
|
||||
if (!base)
|
||||
return null;
|
||||
const url = new URL(base);
|
||||
url.pathname = url.pathname.replace(/\/$/, '') + '/' + route.replace(/^\//, '');
|
||||
url.search = '';
|
||||
url.hash = '';
|
||||
return url.href;
|
||||
};
|
||||
|
||||
const pollServiceStatus = () => getServiceStatus().then(running => {
|
||||
const node = document.getElementById('service_status');
|
||||
const endpoint = L.hasViewPermission() ? getEffectiveWebEndpoint('openlist2') : { protocol: 'http:', port: '' };
|
||||
const site = L.hasViewPermission() ? uci.get('openlist2', '@openlist2[0]', 'site_url') : '';
|
||||
if (node)
|
||||
dom.content(node, renderStatus(running, endpoint.protocol, endpoint.port, site));
|
||||
});
|
||||
|
||||
const renderServiceSection = () => {
|
||||
poll.add(pollServiceStatus);
|
||||
return E('div', { 'class': 'cbi-section', 'id': 'status_bar' },
|
||||
E('p', { 'id': 'service_status' }, _('Collecting data...')));
|
||||
};
|
||||
|
||||
const copyPassword = async value => {
|
||||
if (typeof navigator !== 'undefined' && navigator.clipboard) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
return true;
|
||||
} catch (e) { }
|
||||
}
|
||||
const field = E('textarea', { 'style': 'position:fixed;left:-10000px' }, value);
|
||||
document.body.appendChild(field);
|
||||
try {
|
||||
field.select();
|
||||
return document.execCommand('copy') === true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
} finally {
|
||||
document.body.removeChild(field);
|
||||
}
|
||||
};
|
||||
|
||||
return view.extend({
|
||||
load() {
|
||||
return Promise.all([
|
||||
uci.load('openlist2')
|
||||
]);
|
||||
// UCI contains credentials; readers may view status and bounded logs only.
|
||||
return L.hasViewPermission() ? uci.load('openlist2') : Promise.resolve();
|
||||
},
|
||||
|
||||
async handleResetPassword(data) {
|
||||
const data_dir = uci.get(data[0], '@openlist2[0]', 'data_dir') || '/etc/openlist2';
|
||||
async handleResetPassword() {
|
||||
if (!L.hasViewPermission())
|
||||
return;
|
||||
try {
|
||||
const newpassword = await fs.exec('/usr/bin/openlist2', ['admin', 'random', '--data', data_dir]);
|
||||
const new_password = newpassword.stdout.match(/password:\s*(\S+)/)[1];
|
||||
const result = await fs.exec(HELPER, ['password-reset']);
|
||||
const output = (result.stdout || '').replace(/\u001b\[[0-9;]*m/g, '');
|
||||
const username = output.match(/^username:\s*(.+)$/m);
|
||||
const password = output.match(/^password:\s*(\S+)\s*$/m);
|
||||
if (result.code !== 0 || !username || !password)
|
||||
throw new Error((result.stderr || _('The password reset did not return a username and password.')).trim());
|
||||
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = new_password;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textArea);
|
||||
alert(`${_('Username:')}admin\n${_('New Password:')}${new_password}\n\n${_('New password has been copied to clipboard.')}`);
|
||||
const newPassword = password[1];
|
||||
const message = E('p', { 'role': 'status' }, _('Copy the new password before closing this dialog.'));
|
||||
const copy = async () => {
|
||||
message.textContent = await copyPassword(newPassword)
|
||||
? _('New password has been copied to clipboard.')
|
||||
: _('Automatic copy failed. Select and copy the password manually.');
|
||||
};
|
||||
ui.showModal(_('Password reset'), [
|
||||
E('p', {}, _('Username:') + ' ' + username[1].trim()),
|
||||
E('label', {}, _('New Password:')),
|
||||
E('input', { 'type': 'text', 'readonly': 'readonly', 'value': newPassword, 'style': 'width:100%' }),
|
||||
message,
|
||||
E('div', { 'class': 'right' }, [
|
||||
E('button', { 'class': 'btn cbi-button-action', 'click': ui.createHandlerFn(this, copy) }, _('Copy password')),
|
||||
' ',
|
||||
E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Close'))
|
||||
])
|
||||
]);
|
||||
await copy();
|
||||
} catch (error) {
|
||||
console.error('Failed to reset password: ', error);
|
||||
ui.addNotification(null, E('p', _('Unable to reset the password: %s').format(error.message)));
|
||||
}
|
||||
},
|
||||
|
||||
render(data) {
|
||||
let m, s, o, httpsPortOption;
|
||||
const endpoint = getEffectiveWebEndpoint(data[0]);
|
||||
const webport = endpoint.port;
|
||||
const protocol = endpoint.protocol;
|
||||
const site_url = uci.get(data[0], '@openlist2[0]', 'site_url') || '';
|
||||
render() {
|
||||
if (!L.hasViewPermission())
|
||||
return E('div', {}, [
|
||||
renderServiceSection(),
|
||||
E('p', {}, _('Configuration contains credentials and is available only to users with write access.'))
|
||||
]);
|
||||
let m, s, o;
|
||||
|
||||
m = new form.Map('openlist2', _('OpenList'),
|
||||
_('A file list program that supports multiple storage.') + '<br />' +
|
||||
@@ -188,21 +277,28 @@ return view.extend({
|
||||
s.anonymous = true;
|
||||
s.addremove = false;
|
||||
|
||||
s.render = () => {
|
||||
poll.add(() => {
|
||||
return L.resolveDefault(getServiceStatus()).then(res => {
|
||||
const view = document.getElementById('service_status');
|
||||
if (view)
|
||||
dom.content(view, renderStatus(res, protocol, webport, site_url));
|
||||
});
|
||||
});
|
||||
|
||||
return E('div', { class: 'cbi-section', id: 'status_bar' }, [
|
||||
E('p', { id: 'service_status' }, _('Collecting data...'))
|
||||
]);
|
||||
};
|
||||
s.render = renderServiceSection;
|
||||
|
||||
s = m.section(form.NamedSection, '@openlist2[0]', 'openlist2');
|
||||
const fields = {};
|
||||
const addOption = (tab, type, name, ...args) => {
|
||||
const option = s.taboption(tab, type, name, ...args);
|
||||
fields[name] = option;
|
||||
return option;
|
||||
};
|
||||
const fieldValue = (section_id, name, fallback = '') => {
|
||||
const value = fields[name] && fields[name].formvalue(section_id);
|
||||
return value == null ? uci.get('openlist2', section_id, name) ?? fields[name]?.default ?? fallback : value;
|
||||
};
|
||||
const addBackendLink = (tab, name, route, label, description) => {
|
||||
const option = addOption(tab, form.DummyValue, name, label, description);
|
||||
option.renderWidget = () => {
|
||||
const url = backendUrl(route);
|
||||
return url ? E('a', { 'href': url, 'target': '_blank', 'rel': 'noopener noreferrer' }, label)
|
||||
: E('span', {}, _('Configure a web port or Site URL to open the management page.'));
|
||||
};
|
||||
return option;
|
||||
};
|
||||
|
||||
s.tab('basic', _('Basic Settings'));
|
||||
s.tab('global', _('Global Settings'));
|
||||
@@ -210,7 +306,8 @@ return view.extend({
|
||||
s.tab('database', _('Database'));
|
||||
s.tab('search', _('Search'));
|
||||
s.tab('scheme', _('Web Protocol'));
|
||||
s.tab('tasks', _('Task threads'));
|
||||
s.tab('tasks', _('Task threads'),
|
||||
_('Worker counts below only initialize missing settings. After the first start, change worker counts and speed limits in OpenList Management > Settings > Traffic. Upload and decompression-upload tasks do not support persistence.'));
|
||||
s.tab('cors', _('CORS Settings'));
|
||||
s.tab('s3', _('Object Storage'));
|
||||
s.tab('ftp', _('FTP'));
|
||||
@@ -218,103 +315,108 @@ return view.extend({
|
||||
s.tab('mcp', _('MCP'));
|
||||
|
||||
// init
|
||||
o = s.taboption('basic', form.Flag, 'enabled', _('Enabled'));
|
||||
o = addOption('basic', form.Flag, 'enabled', _('Enabled'));
|
||||
o.default = o.disabled;
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('basic', form.Flag, 'debug', _('Debug logging'),
|
||||
o = addOption('basic', form.Flag, 'debug', _('Debug logging'),
|
||||
_('Pass --debug to OpenList and send stderr to procd logs.'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('basic', form.Value, 'delayed_start', _('Delayed Start (seconds)'));
|
||||
o = addOption('basic', form.Value, 'delayed_start', _('Delayed Start (seconds)'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '0';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('basic', form.Flag, 'allow_wan', _('Open firewall port'));
|
||||
o = addOption('basic', form.Flag, 'allow_wan', _('Open firewall port'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('basic', form.Value, 'data_dir', _('Data directory'));
|
||||
o = addOption('basic', form.Value, 'data_dir', _('Data directory'),
|
||||
_('Changing this path does not move data. Stop OpenList, back up and move the complete data directory, then apply the new path.'));
|
||||
o.default = '/etc/openlist2';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('basic', form.Value, 'temp_dir', _('Cache directory'));
|
||||
o = addOption('basic', form.Value, 'temp_dir', _('Cache directory'),
|
||||
_('OpenList clears this directory on startup. Use a dedicated empty subdirectory. Never select a disk mount root or a directory containing files you want to keep.'));
|
||||
o.default = '/tmp/openlist2';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('basic', form.Button, '_newpassword', _('Reset Password'),
|
||||
_('Generate a new random password.'));
|
||||
o = addOption('basic', form.Button, '_newpassword', _('Reset Password'),
|
||||
_('Generate a new random password using the saved and applied data directory. Start OpenList once before using this action.'));
|
||||
o.inputtitle = _('Reset Password');
|
||||
o.inputstyle = 'apply';
|
||||
o.onclick = L.bind(this.handleResetPassword, this, data);
|
||||
o.onclick = ui.createHandlerFn(this, 'handleResetPassword');
|
||||
|
||||
// global
|
||||
o = s.taboption('global', form.Flag, 'force', _('Force read config'),
|
||||
o = addOption('global', form.Flag, 'force', _('Force read config'),
|
||||
_('Setting this to true will force the program to read the configuration file, ignoring environment variables.'));
|
||||
o.default = true;
|
||||
o.default = '1';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('global', form.Value, 'site_url', _('Site URL'),
|
||||
o = addOption('global', form.Value, 'site_url', _('Site URL'),
|
||||
_('When the web is reverse proxied to a subdirectory, this option must be filled out to ensure proper functioning of the web. Do not include \'/\' at the end of the URL'));
|
||||
o.validate = function(section_id, value) {
|
||||
return validateSiteUrl(value);
|
||||
};
|
||||
|
||||
o = s.taboption('global', form.Value, 'cdn', _('CDN URL'));
|
||||
o = addOption('global', form.Value, 'cdn', _('CDN URL'));
|
||||
o.default = '';
|
||||
|
||||
o = s.taboption('global', form.Value, 'jwt_secret', _('JWT Key'));
|
||||
o = addOption('global', form.Value, 'jwt_secret', _('JWT Key'));
|
||||
o.password = true;
|
||||
o.default = '';
|
||||
|
||||
o = s.taboption('global', form.Value, 'token_expires_in', _('Login Validity Period (hours)'));
|
||||
o = addOption('global', form.Value, 'token_expires_in', _('Login Validity Period (hours)'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '48';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('global', form.Value, 'proxy_address', _('Proxy address'),
|
||||
o = addOption('global', form.Value, 'proxy_address', _('Proxy address'),
|
||||
_('HTTP, HTTPS, SOCKS4, SOCKS5 or SOCKS5HOSTNAME proxy used by OpenList outbound requests.'));
|
||||
o.password = true;
|
||||
o.default = '';
|
||||
|
||||
o = s.taboption('global', form.Value, 'auto_memory_limit', _('Auto memory limit (MB)'),
|
||||
o = addOption('global', form.Value, 'auto_memory_limit', _('Auto memory limit (MB)'),
|
||||
_('0 disables the automatic memory limit. The upstream default is 4 MB.'));
|
||||
o.default = '4';
|
||||
o.datatype = 'uinteger';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('global', form.Value, 'min_free_memory', _('Minimum free memory (MB)'),
|
||||
o = addOption('global', form.Value, 'min_free_memory', _('Minimum free memory (MB)'),
|
||||
_('Values below 16 let OpenList calculate a default; negative values disable memory cache.'));
|
||||
o.default = '0';
|
||||
o.datatype = 'integer';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('global', form.Value, 'max_block_limit', _('Maximum block size (MB)'),
|
||||
o = addOption('global', form.Value, 'max_block_limit', _('Maximum block size (MB)'),
|
||||
_('Values below 4 let OpenList calculate a default.'));
|
||||
o.default = '0';
|
||||
o.datatype = 'uinteger';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('global', form.Value, 'max_connections', _('Max Connections'),
|
||||
o = addOption('global', form.Value, 'max_connections', _('Max Connections'),
|
||||
_('0 is unlimited, It is recommend to set a low number of concurrency (10-20) for poor performance device'));
|
||||
o.default = '0';
|
||||
o.datatype = 'uinteger';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('global', form.Value, 'max_concurrency', _('Max concurrency of local proxies'),
|
||||
o = addOption('global', form.Value, 'max_concurrency', _('Max concurrency of local proxies'),
|
||||
_('0 is unlimited, Limit the maximum concurrency of local agents. The default value is 64'));
|
||||
o.default = '64';
|
||||
o.datatype = 'uinteger';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('global', form.Flag, 'tls_insecure_skip_verify', _('Disable TLS Verify'),
|
||||
o = addOption('global', form.Flag, 'tls_insecure_skip_verify', _('Disable TLS Verify'),
|
||||
_('Skip remote TLS certificate verification (not recommended).'));
|
||||
o.default = false;
|
||||
o.default = '0';
|
||||
o.rmempty = false;
|
||||
|
||||
// Logs
|
||||
o = s.taboption('log', form.Flag, 'log', _('Enable Logs'));
|
||||
o.default = 1;
|
||||
o = addOption('log', form.Flag, 'log', _('Enable Logs'));
|
||||
o.default = '1';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('log', form.Value, 'log_path', _('Log path'));
|
||||
o = addOption('log', form.Value, 'log_path', _('Log path'));
|
||||
o.default = '/var/log/openlist2.log';
|
||||
o.rmempty = false;
|
||||
o.depends('log', '1');
|
||||
@@ -325,104 +427,114 @@ return view.extend({
|
||||
return _('Log path must be /var/log/openlist2* without subdirectories.');
|
||||
};
|
||||
|
||||
o = s.taboption('log', form.Value, 'log_max_size', _('Max Size (MB)'));
|
||||
o = addOption('log', form.Value, 'log_max_size', _('Max Size (MB)'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '50';
|
||||
o.default = '1';
|
||||
o.rmempty = false;
|
||||
o.depends('log', '1');
|
||||
|
||||
o = s.taboption('log', form.Value, 'log_max_backups', _('Max backups'));
|
||||
o = addOption('log', form.Value, 'log_max_backups', _('Max backups'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '30';
|
||||
o.default = '3';
|
||||
o.rmempty = false;
|
||||
o.depends('log', '1');
|
||||
|
||||
o = s.taboption('log', form.Value, 'log_max_age', _('Max age'));
|
||||
o = addOption('log', form.Value, 'log_max_age', _('Max age'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '28';
|
||||
o.rmempty = false;
|
||||
o.depends('log', '1');
|
||||
|
||||
o = s.taboption('log', form.Flag, 'log_compress', _('Log Compress'));
|
||||
o.default = 'false';
|
||||
o = addOption('log', form.Flag, 'log_compress', _('Log Compress'));
|
||||
o.default = '0';
|
||||
o.rmempty = false;
|
||||
o.depends('log', '1');
|
||||
|
||||
o = s.taboption('log', form.Flag, 'log_filter', _('Enable common log filters'),
|
||||
o = addOption('log', form.Flag, 'log_filter', _('Enable common log filters'),
|
||||
_('Filter noisy access logs such as health checks, HEAD requests and WebDAV PROPFIND.'));
|
||||
o.rmempty = false;
|
||||
o.depends('log', '1');
|
||||
|
||||
o = s.taboption('log', form.Value, 'log_filter_cidr', _('Custom log filter CIDR'));
|
||||
o = addOption('log', form.Value, 'log_filter_cidr', _('Custom log filter CIDR'));
|
||||
o.depends('log_filter', '1');
|
||||
|
||||
o = s.taboption('log', form.Value, 'log_filter_path', _('Custom log filter path'));
|
||||
o = addOption('log', form.Value, 'log_filter_path', _('Custom log filter path'));
|
||||
o.depends('log_filter', '1');
|
||||
|
||||
o = s.taboption('log', form.Value, 'log_filter_method', _('Custom log filter method'));
|
||||
o = addOption('log', form.Value, 'log_filter_method', _('Custom log filter method'));
|
||||
o.depends('log_filter', '1');
|
||||
|
||||
// database
|
||||
o = s.taboption('database', form.ListValue, 'database_type', _('Database Type'));
|
||||
o = addOption('database', form.ListValue, 'database_type', _('Database Type'));
|
||||
o.default = 'sqlite3';
|
||||
o.value('sqlite3', _('SQLite'));
|
||||
o.value('mysql', _('MySQL'));
|
||||
o.value('postgres', _('PostgreSQL'));
|
||||
|
||||
o = s.taboption('database', form.Value, 'mysql_host', _('Database Host'));
|
||||
o = addOption('database', form.Value, 'mysql_host', _('Database Host'));
|
||||
o.depends('database_type', 'mysql');
|
||||
o.depends('database_type', 'postgres');
|
||||
|
||||
o = s.taboption('database', form.Value, 'mysql_port', _('Database Port'),
|
||||
o = addOption('database', form.Value, 'mysql_port', _('Database Port'),
|
||||
_('Use 0 to apply the usual default for the selected database: 3306 for MySQL, 5432 for PostgreSQL.'));
|
||||
o.datatype = 'port';
|
||||
o.default = '0';
|
||||
o.depends('database_type', 'mysql');
|
||||
o.depends('database_type', 'postgres');
|
||||
|
||||
o = s.taboption('database', form.Value, 'mysql_username', _('Database Username'));
|
||||
o = addOption('database', form.Value, 'mysql_username', _('Database Username'));
|
||||
o.depends('database_type', 'mysql');
|
||||
o.depends('database_type', 'postgres');
|
||||
|
||||
o = s.taboption('database', form.Value, 'mysql_password', _('Database Password'));
|
||||
o = addOption('database', form.Value, 'mysql_password', _('Database Password'));
|
||||
o.password = true;
|
||||
o.depends('database_type', 'mysql');
|
||||
o.depends('database_type', 'postgres');
|
||||
|
||||
o = s.taboption('database', form.Value, 'mysql_database', _('Database Name'));
|
||||
o = addOption('database', form.Value, 'mysql_database', _('Database Name'));
|
||||
o.depends('database_type', 'mysql');
|
||||
o.depends('database_type', 'postgres');
|
||||
|
||||
o = s.taboption('database', form.Value, 'mysql_table_prefix', _('Database Table Prefix'));
|
||||
o = addOption('database', form.Value, 'mysql_table_prefix', _('Database Table Prefix'));
|
||||
o.default = 'x_';
|
||||
o.depends('database_type', 'mysql');
|
||||
o.depends('database_type', 'postgres');
|
||||
|
||||
o = s.taboption('database', form.Value, 'mysql_ssl_mode', _('Database SSL Mode'));
|
||||
o = addOption('database', form.Value, 'mysql_ssl_mode', _('Database SSL Mode'));
|
||||
o.depends('database_type', 'mysql');
|
||||
o.depends('database_type', 'postgres');
|
||||
|
||||
o = s.taboption('database', form.Value, 'mysql_dsn', _('Database DSN'));
|
||||
o = addOption('database', form.Value, 'mysql_dsn', _('Database DSN'));
|
||||
o.password = true;
|
||||
o.depends('database_type', 'mysql');
|
||||
o.depends('database_type', 'postgres');
|
||||
|
||||
o = s.taboption('search', form.Value, 'meilisearch_host', _('Meilisearch Host'));
|
||||
o = addOption('search', form.Value, 'meilisearch_host', _('Meilisearch Host'));
|
||||
o.default = 'http://localhost:7700';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('search', form.Value, 'meilisearch_api_key', _('Meilisearch API Key'));
|
||||
o = addOption('search', form.Value, 'meilisearch_api_key', _('Meilisearch API Key'));
|
||||
o.password = true;
|
||||
|
||||
o = s.taboption('search', form.Value, 'meilisearch_index', _('Meilisearch Index'));
|
||||
o = addOption('search', form.Value, 'meilisearch_index', _('Meilisearch Index'));
|
||||
o.default = 'openlist';
|
||||
o.rmempty = false;
|
||||
|
||||
o = addOption('search', form.Value, 'bleve_dir', _('Bleve index directory'),
|
||||
_('Empty uses the bleve subdirectory of the data directory. Keep the index separate from cache and frontend files.'));
|
||||
o.default = '';
|
||||
|
||||
o = addOption('global', form.Value, 'dist_dir', _('External frontend directory'),
|
||||
_('Optional directory containing index.html from a matching OpenList frontend. Empty uses the bundled frontend. Restart OpenList after changing it.'));
|
||||
o.default = '';
|
||||
|
||||
// scheme
|
||||
o = s.taboption('scheme', form.Value, 'listen_addr', _('Listen address'));
|
||||
o = addOption('scheme', form.Value, 'listen_addr', _('Listen address'));
|
||||
o.default = '0.0.0.0';
|
||||
o.datatype = 'ipaddr';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('scheme', form.Value, 'listen_http_port', _('HTTP listen port'),
|
||||
o = addOption('scheme', form.Value, 'listen_http_port', _('HTTP listen port'),
|
||||
_('The upstream default is 5244. Set to -1 to disable HTTP.'));
|
||||
o.datatype = 'integer';
|
||||
o.default = '5244';
|
||||
@@ -432,7 +544,7 @@ return view.extend({
|
||||
return validateListenPort(value);
|
||||
};
|
||||
|
||||
httpsPortOption = o = s.taboption('scheme', form.Value, 'listen_https_port', _('HTTPS listen port'),
|
||||
o = addOption('scheme', form.Value, 'listen_https_port', _('HTTPS listen port'),
|
||||
_('The upstream default is -1 (disabled). Set a valid port number to enable HTTPS.'));
|
||||
o.datatype = 'integer';
|
||||
o.default = '-1';
|
||||
@@ -442,132 +554,122 @@ return view.extend({
|
||||
return validateListenPort(value);
|
||||
};
|
||||
|
||||
o = s.taboption('scheme', form.Flag, 'force_https', _('Force HTTPS'));
|
||||
o = addOption('scheme', form.Flag, 'force_https', _('Force HTTPS'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('scheme', form.Value, 'ssl_cert', _('SSL cert'),
|
||||
o = addOption('scheme', form.Value, 'ssl_cert', _('SSL cert'),
|
||||
_('SSL certificate file path'));
|
||||
o.validate = function(section_id, value) {
|
||||
return validateHttpsRequiredFile(section_id, httpsPortOption, value,
|
||||
_('SSL certificate file path is required when HTTPS is enabled.'));
|
||||
};
|
||||
|
||||
o = s.taboption('scheme', form.Value, 'ssl_key', _('SSL key'),
|
||||
o = addOption('scheme', form.Value, 'ssl_key', _('SSL key'),
|
||||
_('SSL key file path'));
|
||||
o.validate = function(section_id, value) {
|
||||
return validateHttpsRequiredFile(section_id, httpsPortOption, value,
|
||||
_('SSL key file path is required when HTTPS is enabled.'));
|
||||
};
|
||||
|
||||
o = s.taboption('scheme', form.Value, 'listen_unix_file', _('Unix socket file'));
|
||||
o = addOption('scheme', form.Value, 'listen_unix_file', _('Unix socket file'));
|
||||
|
||||
o = s.taboption('scheme', form.Value, 'listen_unix_file_perm', _('Unix socket permission'));
|
||||
o = addOption('scheme', form.Value, 'listen_unix_file_perm', _('Unix socket permission'));
|
||||
|
||||
o = s.taboption('scheme', form.Flag, 'listen_enable_h2c', _('Enable H2C'),
|
||||
o = addOption('scheme', form.Flag, 'listen_enable_h2c', _('Enable H2C'),
|
||||
_('Enable cleartext HTTP/2 for reverse proxies that use grpc_pass.'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('scheme', form.Flag, 'listen_enable_h3', _('Enable HTTP/3/QUIC'),
|
||||
o = addOption('scheme', form.Flag, 'listen_enable_h3', _('Enable HTTP/3/QUIC'),
|
||||
_('Enable HTTP/3 over QUIC on the HTTPS listen port. HTTPS must be enabled and certificate files must be configured.'));
|
||||
o.rmempty = false;
|
||||
|
||||
// tasks
|
||||
o = s.taboption('tasks', form.Value, 'download_workers', _('Download Workers'));
|
||||
addBackendLink('tasks', '_traffic', '@manage/settings/traffic', _('Open traffic settings'),
|
||||
_('Use the OpenList backend to change the effective worker counts and transfer speed limits.'));
|
||||
o = addOption('tasks', form.Value, 'download_workers', _('Download Workers (initial)'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '5';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('tasks', form.Value, 'download_max_retry', _('Download Max Retry'));
|
||||
o = addOption('tasks', form.Value, 'download_max_retry', _('Download Max Retry'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '1';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('tasks', form.Flag, 'download_task_persistant', _('Download Task Persistence'));
|
||||
o = addOption('tasks', form.Flag, 'download_task_persistant', _('Download Task Persistence'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('tasks', form.Value, 'transfer_workers', _('Transfer Workers'));
|
||||
o = addOption('tasks', form.Value, 'transfer_workers', _('Transfer Workers (initial)'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '5';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('tasks', form.Value, 'transfer_max_retry', _('Transfer Max Retry'));
|
||||
o = addOption('tasks', form.Value, 'transfer_max_retry', _('Transfer Max Retry'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '2';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('tasks', form.Flag, 'transfer_task_persistant', _('Transfer Task Persistence'));
|
||||
o = addOption('tasks', form.Flag, 'transfer_task_persistant', _('Transfer Task Persistence'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('tasks', form.Value, 'upload_workers', _('Upload Workers'));
|
||||
o = addOption('tasks', form.Value, 'upload_workers', _('Upload Workers (initial)'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '5';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('tasks', form.Value, 'upload_max_retry', _('Upload Max Retry'));
|
||||
o = addOption('tasks', form.Value, 'upload_max_retry', _('Upload Max Retry'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '0';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('tasks', form.Flag, 'upload_task_persistant', _('Upload Task Persistence'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('tasks', form.Value, 'copy_workers', _('Copy Workers'));
|
||||
o = addOption('tasks', form.Value, 'copy_workers', _('Copy Workers (initial)'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '5';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('tasks', form.Value, 'copy_max_retry', _('Copy Max Retry'));
|
||||
o = addOption('tasks', form.Value, 'copy_max_retry', _('Copy Max Retry'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '2';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('tasks', form.Flag, 'copy_task_persistant', _('Copy Task Persistence'));
|
||||
o = addOption('tasks', form.Flag, 'copy_task_persistant', _('Copy Task Persistence'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('tasks', form.Value, 'move_workers', _('Move Workers'));
|
||||
o = addOption('tasks', form.Value, 'move_workers', _('Move Workers (initial)'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '5';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('tasks', form.Value, 'move_max_retry', _('Move Max Retry'));
|
||||
o = addOption('tasks', form.Value, 'move_max_retry', _('Move Max Retry'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '2';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('tasks', form.Flag, 'move_task_persistant', _('Move Task Persistence'));
|
||||
o = addOption('tasks', form.Flag, 'move_task_persistant', _('Move Task Persistence'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('tasks', form.Value, 'decompress_workers', _('Decompress Workers'));
|
||||
o = addOption('tasks', form.Value, 'decompress_workers', _('Decompress Workers (initial)'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '5';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('tasks', form.Value, 'decompress_max_retry', _('Decompress Max Retry'));
|
||||
o = addOption('tasks', form.Value, 'decompress_max_retry', _('Decompress Max Retry'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '2';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('tasks', form.Flag, 'decompress_task_persistant', _('Decompress Task Persistence'));
|
||||
o = addOption('tasks', form.Flag, 'decompress_task_persistant', _('Decompress Task Persistence'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('tasks', form.Value, 'decompress_upload_workers', _('Decompress Upload Workers'));
|
||||
o = addOption('tasks', form.Value, 'decompress_upload_workers', _('Decompress Upload Workers (initial)'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '5';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('tasks', form.Value, 'decompress_upload_max_retry', _('Decompress Upload Max Retry'));
|
||||
o = addOption('tasks', form.Value, 'decompress_upload_max_retry', _('Decompress Upload Max Retry'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '2';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('tasks', form.Flag, 'decompress_upload_task_persistant', _('Decompress Upload Task Persistence'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('tasks', form.Flag, 'allow_retry_canceled', _('Allow retry canceled tasks'));
|
||||
o = addOption('tasks', form.Flag, 'allow_retry_canceled', _('Allow retry canceled tasks'));
|
||||
o.rmempty = false;
|
||||
|
||||
// cors
|
||||
o = s.taboption('cors', form.Value, 'cors_allow_origins', _('Allow Origins'),
|
||||
o = addOption('cors', form.Value, 'cors_allow_origins', _('Allow Origins'),
|
||||
_('Comma-separated list. Use * to allow any value.'));
|
||||
o.default = '*';
|
||||
o.rmempty = false;
|
||||
@@ -575,83 +677,190 @@ return view.extend({
|
||||
return validateCorsOrigins(value);
|
||||
};
|
||||
|
||||
o = s.taboption('cors', form.Value, 'cors_allow_methods', _('Allow Methods'),
|
||||
o = addOption('cors', form.Value, 'cors_allow_methods', _('Allow Methods'),
|
||||
_('Comma-separated list. Use * to allow any value.'));
|
||||
o.default = '*';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('cors', form.Value, 'cors_allow_headers', _('Allow Headers'),
|
||||
o = addOption('cors', form.Value, 'cors_allow_headers', _('Allow Headers'),
|
||||
_('Comma-separated list. Use * to allow any value.'));
|
||||
o.default = '*';
|
||||
o.rmempty = false;
|
||||
|
||||
// s3
|
||||
o = s.taboption('s3', form.Flag, 's3', _('Enabled S3'));
|
||||
addBackendLink('s3', '_s3_settings', '@manage/settings/s3', _('Open S3 management'),
|
||||
_('Generate the S3 access key and secret in OpenList settings, then configure bucket mappings. Restart OpenList after changing credentials.'));
|
||||
o = addOption('s3', form.Flag, 's3', _('Enabled S3'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('s3', form.Value, 's3_port', _('Port'));
|
||||
o = addOption('s3', form.Value, 's3_port', _('Port'));
|
||||
o.datatype = 'and(port,min(1))';
|
||||
o.default = 5246;
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('s3', form.Flag, 's3_ssl', _('Enable SSL'));
|
||||
o = addOption('s3', form.Flag, 's3_ssl', _('Enable SSL'));
|
||||
o.rmempty = false;
|
||||
|
||||
// ftp
|
||||
o = s.taboption('ftp', form.Flag, 'ftp', _('Enabled FTP'));
|
||||
addBackendLink('ftp', '_ftp_settings', '@manage/settings/ftp', _('Open FTP settings'),
|
||||
_('Configure the public host and passive port mapping in OpenList. Restart the service after changing FTP settings.'));
|
||||
o = addOption('ftp', form.DynamicList, 'ftp_pasv_ports', _('FTP passive firewall ports'),
|
||||
_('Ports or ranges to allow from WAN, for example 50000-50100. They must match the listening ports in OpenList passive port mapping. Empty means passive ports must be allowed manually.'));
|
||||
o.placeholder = '50000-50100';
|
||||
o.depends({ ftp: '1', allow_wan: '1' });
|
||||
o = addOption('ftp', form.Flag, 'ftp', _('Enabled FTP'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('ftp', form.Value, 'ftp_port', _('FTP Port'));
|
||||
o = addOption('ftp', form.Value, 'ftp_port', _('FTP Port'));
|
||||
o.datatype = 'and(port,min(1))';
|
||||
o.default = 5221;
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('ftp', form.Value, 'find_pasv_port_attempts', _('Max retries on port conflict during passive transfer'));
|
||||
o = addOption('ftp', form.Value, 'find_pasv_port_attempts', _('Max retries on port conflict during passive transfer'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '50';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('ftp', form.Flag, 'active_transfer_port_non_20', _('Enable non-20 port for active transfer'));
|
||||
o = addOption('ftp', form.Flag, 'active_transfer_port_non_20', _('Enable non-20 port for active transfer'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('ftp', form.Value, 'idle_timeout', _('Client idle timeout (seconds)'));
|
||||
o = addOption('ftp', form.Value, 'idle_timeout', _('Client idle timeout (seconds)'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '900';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('ftp', form.Value, 'connection_timeout', _('Connection timeout (seconds)'));
|
||||
o = addOption('ftp', form.Value, 'connection_timeout', _('Connection timeout (seconds)'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '30';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('ftp', form.Flag, 'disable_active_mode', _('Disable active transfer mode'));
|
||||
o = addOption('ftp', form.Flag, 'disable_active_mode', _('Disable active transfer mode'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('ftp', form.Flag, 'default_transfer_binary', _('Enable binary transfer mode'));
|
||||
o = addOption('ftp', form.Flag, 'default_transfer_binary', _('Enable binary transfer mode'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('ftp', form.Flag, 'enable_active_conn_ip_check', _('Client IP check in active transfer mode'));
|
||||
o = addOption('ftp', form.Flag, 'enable_active_conn_ip_check', _('Client IP check in active transfer mode'));
|
||||
o.default = '1';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('ftp', form.Flag, 'enable_pasv_conn_ip_check', _('Client IP check in passive transfer mode'));
|
||||
o = addOption('ftp', form.Flag, 'enable_pasv_conn_ip_check', _('Client IP check in passive transfer mode'));
|
||||
o.default = '1';
|
||||
o.rmempty = false;
|
||||
|
||||
// sftp
|
||||
o = s.taboption('sftp', form.Flag, 'sftp', _('Enabled SFTP'));
|
||||
o = addOption('sftp', form.Flag, 'sftp', _('Enabled SFTP'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('sftp', form.Value, 'sftp_port', _('SFTP Port'));
|
||||
o = addOption('sftp', form.Value, 'sftp_port', _('SFTP Port'));
|
||||
o.datatype = 'and(port,min(1))';
|
||||
o.default = 5222;
|
||||
o.rmempty = false;
|
||||
|
||||
// mcp
|
||||
o = s.taboption('mcp', form.Flag, 'mcp', _('Enabled MCP'),
|
||||
o = addOption('mcp', form.Flag, 'mcp', _('Enabled MCP'),
|
||||
_('Enable MCP (Model Context Protocol) server.'));
|
||||
o.rmempty = false;
|
||||
|
||||
|
||||
o = addOption('mcp', form.DummyValue, '_mcp_endpoint', _('MCP endpoint'),
|
||||
_('Use Streamable HTTP with Authorization: TOKEN, without a Bearer prefix. Use an OpenList user login token; retain MCP-Session-Id for subsequent requests.'));
|
||||
o.depends('mcp', '1');
|
||||
o.renderWidget = () => E('div', {}, [
|
||||
E('code', {}, backendUrl('mcp') || _('Configure a web port or Site URL.')),
|
||||
E('p', {}, E('a', { 'href': 'https://doc.oplist.org/guide/advanced/mcp', 'target': '_blank', 'rel': 'noopener noreferrer' }, _('MCP documentation')))
|
||||
]);
|
||||
|
||||
const validateDirectories = (section_id, changed, value) => {
|
||||
const required = changed === 'data_dir' || changed === 'temp_dir';
|
||||
const valid = validateAbsolutePath(value, required);
|
||||
if (valid !== true)
|
||||
return valid;
|
||||
const get = (name, fallback) => normalizePath(name === changed ? value || fallback : fieldValue(section_id, name, fallback) || fallback);
|
||||
const data = get('data_dir', '/etc/openlist2');
|
||||
const cache = get('temp_dir', '/tmp/openlist2');
|
||||
const index = get('bleve_dir', data + '/bleve');
|
||||
const distValue = changed === 'dist_dir' ? value : fieldValue(section_id, 'dist_dir');
|
||||
const dist = distValue ? normalizePath(distValue) : '';
|
||||
for (const path of [data, cache, index, dist].filter(Boolean))
|
||||
if (isBroadDirectory(path))
|
||||
return _('Use a dedicated subdirectory, not a system directory or disk mount root.');
|
||||
if (/^\/(?:mnt|media)\/[^/]+$/.test(cache))
|
||||
return _('Use a cache subdirectory below the disk mount.');
|
||||
if (containsPath(cache, data) || containsPath(index, data) ||
|
||||
containsPath(cache, index) || containsPath(index, cache))
|
||||
return _('Cache and index directories must be separate and must not contain the data directory.');
|
||||
if (dist && (containsPath(dist, data) || containsPath(dist, cache) ||
|
||||
containsPath(cache, dist) || containsPath(dist, index) || containsPath(index, dist)))
|
||||
return _('Frontend files must be separate from private data, cache and index files.');
|
||||
return true;
|
||||
};
|
||||
for (const name of ['data_dir', 'temp_dir', 'bleve_dir', 'dist_dir'])
|
||||
fields[name].validate = (section_id, value) => validateDirectories(section_id, name, value);
|
||||
|
||||
const tlsRequired = section_id => isEnabledPort(fieldValue(section_id, 'listen_https_port', '-1')) ||
|
||||
(fieldValue(section_id, 's3') === '1' && fieldValue(section_id, 's3_ssl') === '1');
|
||||
for (const name of ['ssl_cert', 'ssl_key']) {
|
||||
fields[name].validate = (section_id, value) => {
|
||||
if (tlsRequired(section_id) && !String(value || '').trim())
|
||||
return _('Certificate and key paths are required for HTTPS or S3 SSL.');
|
||||
return validateAbsolutePath(value, false);
|
||||
};
|
||||
}
|
||||
for (const name of ['force_https', 'listen_enable_h3'])
|
||||
fields[name].validate = (section_id, value) => value !== '1' || isEnabledPort(fieldValue(section_id, 'listen_https_port', '-1'))
|
||||
? true : _('Force HTTPS and HTTP/3 require an enabled HTTPS port.');
|
||||
fields.s3_ssl.validate = (section_id, value) => value !== '1' || fieldValue(section_id, 's3') !== '1' ||
|
||||
(fieldValue(section_id, 'ssl_cert').trim() && fieldValue(section_id, 'ssl_key').trim())
|
||||
? true : _('Certificate and key paths are required for HTTPS or S3 SSL.');
|
||||
fields.listen_unix_file.validate = (section_id, value) => validateAbsolutePath(value, false);
|
||||
fields.listen_unix_file_perm.validate = (section_id, value) => !value || /^[0-7]{3,4}$/.test(value)
|
||||
? true : _('Use 3 or 4 octal digits, for example 660 or 0660.');
|
||||
fields.log_max_size.datatype = 'and(uinteger,min(1))';
|
||||
|
||||
const validatePorts = (section_id, changed, value) => {
|
||||
const get = name => name === changed ? value : fieldValue(section_id, name,
|
||||
name === 'listen_http_port' ? '5244' : name === 'listen_https_port' ? '-1' : '');
|
||||
const names = ['listen_http_port', 'listen_https_port'];
|
||||
if (fieldValue(section_id, 's3') === '1') names.push('s3_port');
|
||||
if (fieldValue(section_id, 'ftp') === '1') names.push('ftp_port');
|
||||
if (fieldValue(section_id, 'sftp') === '1') names.push('sftp_port');
|
||||
const seen = new Set();
|
||||
const usePassive = changed === 'ftp_pasv_ports' ||
|
||||
(fieldValue(section_id, 'ftp') === '1' && fieldValue(section_id, 'allow_wan') === '1');
|
||||
const passive = usePassive ? passiveRanges(changed === 'ftp_pasv_ports' ? value : fieldValue(section_id, 'ftp_pasv_ports')) : [];
|
||||
const passiveValid = validatePassivePorts(passive);
|
||||
if (passiveValid !== true)
|
||||
return passiveValid;
|
||||
for (const name of names) {
|
||||
const candidate = String(get(name)).trim();
|
||||
const valid = validateListenPort(candidate);
|
||||
if (valid !== true)
|
||||
return valid;
|
||||
if (candidate === '-1')
|
||||
continue;
|
||||
const port = Number(candidate);
|
||||
if (seen.has(port))
|
||||
return _('Enabled services cannot share TCP port %s.').format(port);
|
||||
seen.add(port);
|
||||
if (fieldValue(section_id, 'ftp') === '1')
|
||||
for (const range of passive) {
|
||||
const [first, last = first] = range.split('-').map(Number);
|
||||
if (port >= first && port <= last)
|
||||
return _('FTP passive ports must not overlap service port %s.').format(port);
|
||||
}
|
||||
}
|
||||
if (!isEnabledPort(get('listen_https_port')) &&
|
||||
(fieldValue(section_id, 'force_https') === '1' || fieldValue(section_id, 'listen_enable_h3') === '1'))
|
||||
return _('Force HTTPS and HTTP/3 require an enabled HTTPS port.');
|
||||
return true;
|
||||
};
|
||||
for (const name of ['listen_http_port', 'listen_https_port', 's3_port', 'ftp_port', 'sftp_port', 'ftp_pasv_ports'])
|
||||
fields[name].validate = (section_id, value) => validatePorts(section_id, name, value);
|
||||
for (const name of ['ftp_pasv_ports', 'log_path', 'log_max_size', 'log_max_backups', 'log_max_age',
|
||||
'log_compress', 'log_filter', 'log_filter_cidr', 'log_filter_path', 'log_filter_method'])
|
||||
fields[name].retain = true;
|
||||
|
||||
return m.render();
|
||||
}
|
||||
});
|
||||
@@ -1,91 +1,72 @@
|
||||
'use strict';
|
||||
'require fs';
|
||||
'require poll';
|
||||
'require uci';
|
||||
'require ui';
|
||||
'require view';
|
||||
|
||||
const DEFAULT_LOG_PATH = '/var/log/openlist2.log';
|
||||
const HELPER = '/usr/libexec/openlist2-helper';
|
||||
let logTextarea, errorMessage;
|
||||
|
||||
let scrollPosition = 0;
|
||||
let userScrolled = false;
|
||||
let logTextarea;
|
||||
let log_path = DEFAULT_LOG_PATH;
|
||||
|
||||
const isSafeLogPath = value => /^\/var\/log\/openlist2(?:[._-][A-Za-z0-9._-]+)?$/.test(value || '');
|
||||
|
||||
const formatLog = res => {
|
||||
const log = (res || '').trim().replace(/\u001b\[[0-9;]*m/g, '');
|
||||
|
||||
return log || _('No log data.');
|
||||
};
|
||||
const formatLog = text => (text || '').replace(/\u001b\[[0-9;]*m/g, '').trim() || _('No log data.');
|
||||
|
||||
const pollLog = () => {
|
||||
if (!logTextarea)
|
||||
return Promise.resolve();
|
||||
|
||||
return fs.read_direct(log_path, 'text')
|
||||
.then(formatLog)
|
||||
.catch(() => _('No log data.'))
|
||||
.then(log => {
|
||||
logTextarea.value = log;
|
||||
return fs.exec(HELPER, ['log-read']).then(result => {
|
||||
if (result.code !== 0)
|
||||
throw new Error((result.stderr || _('Unable to read the log.')).trim());
|
||||
|
||||
if (!userScrolled) {
|
||||
logTextarea.scrollTop = logTextarea.scrollHeight;
|
||||
} else {
|
||||
logTextarea.scrollTop = scrollPosition;
|
||||
}
|
||||
});
|
||||
// Decide before replacing text, so programmatic scrolling is not
|
||||
// mistaken for the user scrolling away from the bottom.
|
||||
const atBottom = logTextarea.scrollTop + logTextarea.clientHeight >= logTextarea.scrollHeight - 10;
|
||||
const position = logTextarea.scrollTop;
|
||||
logTextarea.value = formatLog(result.stdout);
|
||||
logTextarea.scrollTop = atBottom ? logTextarea.scrollHeight : position;
|
||||
errorMessage.textContent = '';
|
||||
errorMessage.hidden = true;
|
||||
}).catch(error => {
|
||||
errorMessage.textContent = _('Unable to read the log: %s').format(error.message);
|
||||
errorMessage.hidden = false;
|
||||
});
|
||||
};
|
||||
|
||||
return view.extend({
|
||||
load() {
|
||||
return uci.load('openlist2');
|
||||
},
|
||||
|
||||
handleCleanLogs() {
|
||||
return fs.write(log_path, '')
|
||||
.then(pollLog)
|
||||
.catch(e => {
|
||||
ui.addNotification(null, E('p', e.message));
|
||||
});
|
||||
if (!L.hasViewPermission())
|
||||
return Promise.resolve();
|
||||
return fs.exec(HELPER, ['log-clear']).then(result => {
|
||||
if (result.code !== 0)
|
||||
throw new Error((result.stderr || _('Unable to clear the log.')).trim());
|
||||
return pollLog();
|
||||
}).catch(error => ui.addNotification(null, E('p', error.message)));
|
||||
},
|
||||
|
||||
render() {
|
||||
const configuredLogPath = uci.get('openlist2', '@openlist2[0]', 'log_path');
|
||||
log_path = isSafeLogPath(configuredLogPath) ? configuredLogPath : DEFAULT_LOG_PATH;
|
||||
|
||||
logTextarea = E('textarea', {
|
||||
'id': 'log_content',
|
||||
'class': 'cbi-input-textarea',
|
||||
'wrap': 'off',
|
||||
'readonly': 'readonly',
|
||||
'style': 'width: calc(100% - 20px);height: 535px;margin: 10px;overflow-y: scroll;',
|
||||
});
|
||||
'style': 'width:100%;height:535px;overflow:auto;'
|
||||
}, _('Collecting data...'));
|
||||
errorMessage = E('p', { 'class': 'alert-message warning', 'role': 'status', 'hidden': true });
|
||||
const clearButton = E('button', {
|
||||
'class': 'btn cbi-button-action',
|
||||
'type': 'button',
|
||||
'disabled': !L.hasViewPermission(),
|
||||
'click': ui.createHandlerFn(this, 'handleCleanLogs')
|
||||
}, _('Clear current log'));
|
||||
|
||||
logTextarea.addEventListener('scroll', () => {
|
||||
userScrolled = true;
|
||||
scrollPosition = logTextarea.scrollTop;
|
||||
});
|
||||
|
||||
const log_textarea_wrapper = E('div', { 'id': 'log_textarea' }, logTextarea);
|
||||
|
||||
setTimeout(() => {
|
||||
poll.add(pollLog);
|
||||
}, 100);
|
||||
|
||||
const clear_logs_button = E('input', { 'class': 'btn cbi-button-action', 'type': 'button', 'style': 'margin-left: 10px; margin-top: 10px;', 'value': _('Clear logs') });
|
||||
clear_logs_button.addEventListener('click', L.bind(this.handleCleanLogs, this));
|
||||
|
||||
return E([
|
||||
E('div', { 'class': 'cbi-map' }, [
|
||||
E('div', { 'class': 'cbi-section' }, [
|
||||
clear_logs_button,
|
||||
log_textarea_wrapper,
|
||||
E('div', { 'style': 'text-align:right' },
|
||||
E('small', {}, _('Refresh every %s seconds.').format(L.env.pollinterval))
|
||||
)
|
||||
])
|
||||
poll.add(pollLog);
|
||||
return E('div', { 'class': 'cbi-map' }, [
|
||||
E('div', { 'class': 'cbi-section' }, [
|
||||
E('p', {}, _('Showing up to the last 200 lines and 16 KiB. Older entries remain in the log files.')),
|
||||
clearButton,
|
||||
errorMessage,
|
||||
logTextarea,
|
||||
E('p', { 'style': 'text-align:right' },
|
||||
_('Refresh every %s seconds.').format(L.env.pollinterval))
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user