🎉 Sync 2026-07-23 18:16:40

This commit is contained in:
github-actions[bot] committed 2026-07-23 18:16:40 +08:00
1 parent 4e2583c9a2
commit aecee003c5
19 files changed
+2559 -941

No files matched your search

@@ -1,4 +1,5 @@
'use strict';
'require dom';
'require form';
'require fs';
'require poll';
@@ -23,24 +24,128 @@ const getServiceStatus = () => {
});
};
const renderStatus = (isRunning, protocol, webport, site_url) => {
const spanTemp = '<em><span style="color:%s"><strong>%s %s</strong></span></em>';
let renderHTML;
if (isRunning) {
let buttonUrl = '';
if (site_url && site_url.trim() !== '') {
buttonUrl = site_url;
} else {
buttonUrl = `${protocol}//${window.location.hostname}:${webport}/`;
}
const button = String.format('<input class="cbi-button-reload" type="button" style="margin-left: 50px" value="%s" onclick="window.open(\'%s\')">',
_('Open Web Interface'), buttonUrl);
renderHTML = spanTemp.format('green', 'OpenList', _('RUNNING')) + button;
} else {
renderHTML = spanTemp.format('red', 'OpenList', _('NOT RUNNING'));
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);
if (url.protocol === 'http:' || url.protocol === 'https:')
return url.href;
} catch (e) { }
}
return renderHTML;
return fallbackUrl;
};
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) {
const buttonUrl = getWebInterfaceUrl(protocol, webport, site_url);
const button = E('input', {
'class': 'cbi-button-reload',
'type': 'button',
'style': 'margin-left: 50px',
'value': _('Open Web Interface')
});
button.addEventListener('click', () => window.open(buttonUrl, '_blank', 'noopener'));
return E('span', {}, [status, button]);
}
return status;
};
const isSafeLogPath = 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 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;
};
const validateListenPort = value => {
const port = (value || '').trim();
if (port === '-1')
return true;
if (/^\d+$/.test(port)) {
const number = Number(port);
if (number >= 1 && number <= 65535)
return true;
}
return _('Port must be -1 or a valid port number from 1 to 65535.');
};
const isHttpUrl = value => {
try {
const url = new URL(value);
return !!url.hostname && (url.protocol === 'http:' || url.protocol === 'https:');
} catch (e) {
return false;
}
};
const validateSiteUrl = value => {
const url = (value || '').trim();
if (!url)
return true;
if (isHttpUrl(url) && !url.endsWith('/'))
return true;
return _('Site URL must start with http:// or https:// and must not end with /.');
};
const validateCorsOrigins = value => {
const origins = (value || '').split(',')
.map(origin => origin.trim())
.filter(Boolean);
for (const origin of origins) {
if (origin === '*' || isHttpUrl(origin))
continue;
return _('CORS origins must be * or include http:// or https://.');
}
return true;
};
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';
if (isEnabledPort(httpsPort))
return { protocol: 'https:', port: httpsPort };
if (isEnabledPort(httpPort))
return { protocol: 'http:', port: httpPort };
return { protocol: 'http:', port: '' };
};
return view.extend({
@@ -69,19 +174,15 @@ return view.extend({
},
render(data) {
let m, s, o;
const webport = uci.get(data[0], '@openlist2[0]', 'port') || '5244';
const ssl = uci.get(data[0], '@openlist2[0]', 'ssl') || '0';
let protocol;
if (ssl === '0') {
protocol = 'http:';
} else if (ssl === '1') {
protocol = 'https:';
}
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') || '';
m = new form.Map('openlist2', _('OpenList'),
_('A file list program that supports multiple storage.'));
_('A file list program that supports multiple storage.') + '<br />' +
_('Initial login username is "%s" and password is "%s". Change it immediately after first login.').format('admin', 'admin'));
s = m.section(form.TypedSection);
s.anonymous = true;
@@ -92,7 +193,7 @@ return view.extend({
return L.resolveDefault(getServiceStatus()).then(res => {
const view = document.getElementById('service_status');
if (view)
view.innerHTML = renderStatus(res, protocol, webport, site_url);
dom.content(view, renderStatus(res, protocol, webport, site_url));
});
});
@@ -107,6 +208,7 @@ return view.extend({
s.tab('global', _('Global Settings'));
s.tab('log', _('Logs'));
s.tab('database', _('Database'));
s.tab('search', _('Search'));
s.tab('scheme', _('Web Protocol'));
s.tab('tasks', _('Task threads'));
s.tab('cors', _('CORS Settings'));
@@ -119,9 +221,8 @@ return view.extend({
o.default = o.disabled;
o.rmempty = false;
o = s.taboption('basic', form.Value, 'port', _('Port'));
o.datatype = 'and(port,min(1))';
o.default = '5244';
o = s.taboption('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)'));
@@ -153,6 +254,9 @@ return view.extend({
o = s.taboption('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.default = '';
@@ -165,6 +269,28 @@ return view.extend({
o.default = '48';
o.rmempty = false;
o = s.taboption('global', form.Value, 'proxy_address', _('Proxy address'),
_('HTTP, HTTPS, SOCKS4, SOCKS5 or SOCKS5HOSTNAME proxy used by OpenList outbound requests.'));
o.default = '';
o = s.taboption('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)'),
_('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)'),
_('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'),
_('0 is unlimited, It is recommend to set a low number of concurrency (10-20) for poor performance device'));
o.default = '0';
@@ -172,13 +298,14 @@ return view.extend({
o.rmempty = false;
o = s.taboption('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 = '0';
_('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.default = true;
o = s.taboption('global', form.Flag, 'tls_insecure_skip_verify', _('Disable TLS Verify'),
_('Skip remote TLS certificate verification (not recommended).'));
o.default = false;
o.rmempty = false;
// Logs
@@ -190,16 +317,22 @@ return view.extend({
o.default = '/var/log/openlist2.log';
o.rmempty = false;
o.depends('log', '1');
o.validate = function(section_id, value) {
if (isSafeLogPath(value))
return true;
return _('Log path must be /var/log/openlist2* without subdirectories.');
};
o = s.taboption('log', form.Value, 'log_max_size', _('Max Size (MB)'));
o.datatype = 'uinteger';
o.default = '10';
o.default = '50';
o.rmempty = false;
o.depends('log', '1');
o = s.taboption('log', form.Value, 'log_max_backups', _('Max backups'));
o.datatype = 'uinteger';
o.default = '5';
o.default = '30';
o.rmempty = false;
o.depends('log', '1');
@@ -214,6 +347,20 @@ return view.extend({
o.rmempty = false;
o.depends('log', '1');
o = s.taboption('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.depends('log_filter', '1');
o = s.taboption('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.depends('log_filter', '1');
// database
o = s.taboption('database', form.ListValue, 'database_type', _('Database Type'));
o.default = 'sqlite3';
@@ -225,9 +372,10 @@ return view.extend({
o.depends('database_type', 'mysql');
o.depends('database_type', 'postgres');
o = s.taboption('database', form.Value, 'mysql_port', _('Database Port'));
o = s.taboption('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 = '3306';
o.default = '0';
o.depends('database_type', 'mysql');
o.depends('database_type', 'postgres');
@@ -256,23 +404,71 @@ return view.extend({
o.depends('database_type', 'mysql');
o.depends('database_type', 'postgres');
// scheme
o = s.taboption('scheme', form.Flag, 'ssl', _('Enable SSL'));
o = s.taboption('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.password = true;
o = s.taboption('search', form.Value, 'meilisearch_index', _('Meilisearch Index'));
o.default = 'openlist';
o.rmempty = false;
// scheme
o = s.taboption('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'),
_('The upstream default is 5244. Set to -1 to disable HTTP.'));
o.datatype = 'integer';
o.default = '5244';
o.rmempty = false;
o.cfgvalue = section_id => getListenPortValue(section_id, 'listen_http_port', '5244');
o.validate = function(section_id, value) {
return validateListenPort(value);
};
httpsPortOption = o = s.taboption('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';
o.rmempty = false;
o.cfgvalue = section_id => getListenPortValue(section_id, 'listen_https_port', '-1');
o.validate = function(section_id, value) {
return validateListenPort(value);
};
o = s.taboption('scheme', form.Flag, 'force_https', _('Force HTTPS'));
o.rmempty = false;
o.depends('ssl', '1');
o = s.taboption('scheme', form.Value, 'ssl_cert', _('SSL cert'),
_('SSL certificate file path'));
o.rmempty = false;
o.depends('ssl', '1');
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'),
_('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 = s.taboption('scheme', form.Value, 'listen_unix_file_perm', _('Unix socket permission'));
o = s.taboption('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'),
_('Enable HTTP/3 over QUIC on the HTTPS listen port. HTTPS must be enabled and certificate files must be configured.'));
o.rmempty = false;
o.depends('ssl', '1');
// tasks
o = s.taboption('tasks', form.Value, 'download_workers', _('Download Workers'));
@@ -285,6 +481,9 @@ return view.extend({
o.default = '1';
o.rmempty = false;
o = s.taboption('tasks', form.Flag, 'download_task_persistant', _('Download Task Persistence'));
o.rmempty = false;
o = s.taboption('tasks', form.Value, 'transfer_workers', _('Transfer Workers'));
o.datatype = 'uinteger';
o.default = '5';
@@ -295,6 +494,9 @@ return view.extend({
o.default = '2';
o.rmempty = false;
o = s.taboption('tasks', form.Flag, 'transfer_task_persistant', _('Transfer Task Persistence'));
o.rmempty = false;
o = s.taboption('tasks', form.Value, 'upload_workers', _('Upload Workers'));
o.datatype = 'uinteger';
o.default = '5';
@@ -305,6 +507,9 @@ return view.extend({
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.datatype = 'uinteger';
o.default = '5';
@@ -315,16 +520,67 @@ return view.extend({
o.default = '2';
o.rmempty = false;
o = s.taboption('tasks', form.Flag, 'copy_task_persistant', _('Copy Task Persistence'));
o.rmempty = false;
o = s.taboption('tasks', form.Value, 'move_workers', _('Move Workers'));
o.datatype = 'uinteger';
o.default = '5';
o.rmempty = false;
o = s.taboption('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.rmempty = false;
o = s.taboption('tasks', form.Value, 'decompress_workers', _('Decompress Workers'));
o.datatype = 'uinteger';
o.default = '5';
o.rmempty = false;
o = s.taboption('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.rmempty = false;
o = s.taboption('tasks', form.Value, 'decompress_upload_workers', _('Decompress Upload Workers'));
o.datatype = 'uinteger';
o.default = '5';
o.rmempty = false;
o = s.taboption('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.rmempty = false;
// cors
o = s.taboption('cors', form.Value, 'cors_allow_origins', _('Allow Origins'));
o = s.taboption('cors', form.Value, 'cors_allow_origins', _('Allow Origins'),
_('Comma-separated list. Use * to allow any value.'));
o.default = '*';
o.rmempty = false;
o.validate = function(section_id, value) {
return validateCorsOrigins(value);
};
o = s.taboption('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_methods', _('Allow Methods'));
o.default = '*';
o.rmempty = false;
o = s.taboption('cors', form.Value, 'cors_allow_headers', _('Allow Headers'));
o = s.taboption('cors', form.Value, 'cors_allow_headers', _('Allow Headers'),
_('Comma-separated list. Use * to allow any value.'));
o.default = '*';
o.rmempty = false;
@@ -364,7 +620,7 @@ return view.extend({
o = s.taboption('ftp', form.Value, 'connection_timeout', _('Connection timeout (seconds)'));
o.datatype = 'uinteger';
o.default = '900';
o.default = '30';
o.rmempty = false;
o = s.taboption('ftp', form.Flag, 'disable_active_mode', _('Disable active transfer mode'));
@@ -374,9 +630,11 @@ return view.extend({
o.rmempty = false;
o = s.taboption('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.default = '1';
o.rmempty = false;
// sftp
@@ -1,42 +1,60 @@
'use strict';
'require dom';
'require fs';
'require poll';
'require uci';
'require ui';
'require view';
const DEFAULT_LOG_PATH = '/var/log/openlist2.log';
let scrollPosition = 0;
let userScrolled = false;
let logTextarea;
let log_path;
let log_path = DEFAULT_LOG_PATH;
uci.load('openlist2').then(() => {
log_path = uci.get('openlist2', '@openlist2[0]', 'log_path') || '/var/log/openlist2.log';
});
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 pollLog = () => {
return Promise.all([
fs.read_direct(log_path, 'text').then(res => {
return res.trim().split(/\n/).join('\n').replace(/\u001b\[33mWARN\u001b\[0m/g, '').replace(/\u001b\[36mINFO\u001b\[0m/g, '').replace(/\u001b\[31mERRO\u001b\[0m/g, '');
}),
]).then(data => {
logTextarea.value = data[0] || _('No log data.');
if (!logTextarea)
return Promise.resolve();
if (!userScrolled) {
logTextarea.scrollTop = logTextarea.scrollHeight;
} else {
logTextarea.scrollTop = scrollPosition;
}
});
return fs.read_direct(log_path, 'text')
.then(formatLog)
.catch(() => _('No log data.'))
.then(log => {
logTextarea.value = log;
if (!userScrolled) {
logTextarea.scrollTop = logTextarea.scrollHeight;
} else {
logTextarea.scrollTop = scrollPosition;
}
});
};
return view.extend({
load() {
return uci.load('openlist2');
},
handleCleanLogs() {
return fs.write(log_path, '')
.catch(e => { ui.addNotification(null, E('p', e.message)) });
.then(pollLog)
.catch(e => {
ui.addNotification(null, E('p', e.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',