🌴 Sync 2026-03-05 23:51:42

This commit is contained in:
github-actions[bot] committed 2026-03-05 23:51:42 +08:00
1 parent 97c049ed0a
commit 0acff03804
14253 files changed
+3893239

No files matched your search

+16
View File
@@ -0,0 +1,16 @@
# This is free software, licensed under the Apache License, Version 2.0 .
include $(TOPDIR)/rules.mk
LUCI_TITLE:=MergerFS
LUCI_DEPENDS:=+luci-base +mergerfs
LUCI_PKGARCH:=all
PKG_VERSION:=1.0.3-r1
PKG_RELEASE:=1
PKG_MAINTAINER:=jjm2473 <jjm2473@gmail.com>
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature
@@ -0,0 +1,256 @@
'use strict';
'require view';
'require fs';
'require ui';
'require uci';
'require rpc';
'require form';
var callMountPoints = function(){
return (rpc.declare({
object: 'luci',
method: 'getMountPoints',
expect: { result: [] }
}))().then(function(mounts) {
// Filter out mounts that are not managed by mergerfs
return (mounts || []).filter(function(mount) {
return mount.device && mount.device.startsWith('mergerfs:/');
});
});
};
return view.extend({
handleUmount: function(m, mounts, i, ev) {
return fs.exec('/bin/umount', [mounts[i].mount])
.then(function(res){
if (res.code === 0)
mounts.splice(i, 1);
else
throw new Error(res.stderr || res.message || _('Failed to unmount'));
})
.then(L.bind(m.render, m))
.catch(function(e) { ui.addNotification(null, E('p', e.message)) });
},
handleReload: function(m, mounts, ev) {
return fs.exec('/etc/init.d/mergerfs', ['reload'])
.then(()=>
new Promise(function(resolve) {
setTimeout(resolve, 200);
})
)
.then(callMountPoints)
.then(function(newMounts) {
if (newMounts && newMounts.length > 0) {
mounts.splice(0, mounts.length, ...newMounts);
} else {
mounts.length = 0; // Clear the mounts if no new ones found
}
})
.then(L.bind(m.render, m))
.catch(function(e) { ui.addNotification(null, E('p', e.message)) });
},
load: function() {
return Promise.all([
callMountPoints(),
]);
},
render: function(data) {
var m, s, o;
m = new form.Map('mergerfs', _('MergerFS Pool'), _('MergerFS is a union filesystem, which allows you to pool multiple directories into a single mount point.<br>This is useful for combining storage space and files from multiple devices or directories into one accessible location.<br>New files can be distributed across the pooled directories based on various policies, such as most free space or first found.<br>More information can be found at <a target="_blank" href="https://github.com/trapexit/mergerfs">GitHub</a>.'));
var mounts = data[0]||[];
// Mount status table
s = m.section(form.GridSection, '_mtab');
s.render = L.bind(function(view, section_id) {
var desc = E('div', { 'class': 'cbi-section-descr' },
_('If mounting fails, search "mergerfs" in the <a href="/cgi-bin/luci/admin/status/logs">system log</a> for more information.')
);
var reload = E('div', { 'class': 'cbi-section-node' }, [
E('button', {
'class': 'btn cbi-button-reload',
'click': ui.createHandlerFn(view, 'handleReload', m, mounts),
}, [ _('Mount Configured MergerFS') ])
]);
var table = E('table', { 'class': 'table cbi-section-table' }, [
E('tr', { 'class': 'tr table-titles' }, [
E('th', { 'class': 'th' }, _('Mount Point')),
E('th', { 'class': 'th center' }, _('Available')),
E('th', { 'class': 'th center' }, _('Used')),
E('th', { 'class': 'th' }, _('Unmount'))
])
]);
var rows = [];
for (var i = 0; i < mounts.length; i++) {
var used = mounts[i].size - mounts[i].free,
umount = true;
if (/^\/(overlay|rom|tmp(?:\/.+)?|dev(?:\/.+)?|)$/.test(mounts[i].mount))
umount = false;
rows.push([
mounts[i].mount,
'%1024.2mB / %1024.2mB'.format(mounts[i].avail, mounts[i].size),
'%.2f%% (%1024.2mB)'.format(100 / mounts[i].size * used, used),
umount ? E('button', {
'class': 'btn cbi-button-remove',
'click': ui.createHandlerFn(view, 'handleUmount', m, mounts, i),
'disabled': this.map.readonly || null
}, [ _('Unmount') ]) : '-'
]);
}
cbi_update_table(table, rows, E('em', _('No mounted MergerFS found')));
return E('div', { 'class': 'cbi-section cbi-tblsection' }, [ E('h3', _('Mounted MergerFS')), desc, reload, table ]);
}, s, this);
// 基本信息分区
s = m.section(form.GridSection, 'pool', _('Pool Settings'));
s.anonymous = true;
s.addremove = true;
o = s.option(form.Flag, 'enabled', _('Enabled'));
o.rmempty = false;
o.default = true;
o.editable = true;
// type: textInput, name: mountpoint
o = s.option(form.Value, 'mountpoint', _('Mount Point'), _('Mount to this path'));
o.datatype = 'string';
o.placeholder = '/mnt/mergerfs';
o.rmempty = false;
// o.editable = true;
o.validate = function(section_id, value) {
value = value ? value.trim() : value;
if (!value || !value.startsWith('/') || value === '/') {
return _('Mount Point is required, must start with "/" and cannot be root "/"');
}
return true;
};
o.cfgvalue = function(section_id) {
var v = uci.get('mergerfs', section_id, 'mountpoint');
return v ? v.trim() : v;
};
o.write = function(section_id, formvalue) {
return uci.set('mergerfs', section_id, 'mountpoint', formvalue ? formvalue.trim() : formvalue);
};
var clean_paths = function(val) {
if (!val || typeof val !== 'string') return val;
return val.split('\n').map(function(line) {
line = line.trim();
if (line.startsWith('#')) return line;
if (line !== '/' && line.endsWith('/')) {
return line.slice(0, -1);
}
return line;
}).join('\n');
};
// type: textarea, name: paths
o = s.option(form.TextValue, 'paths', _('Paths'), _("List of paths to merge, one per line. Line starting with '#' will be treated as a comment.<br>All paths must existed before mounting or the mounting will fail."));
o.datatype = 'string';
o.modalonly = true;
o.rows = 5;
o.placeholder = _('Put each folder on a new line.');
o.validate = function(section_id, value) {
var paths = clean_paths(value).split('\n');
if (!paths.every(function(line) { return line.length === 0 || line.startsWith('/') || line.startsWith('#'); })) {
return _("Each line must be a path starting with a '/' or be a comment starting with '#'.");
}
if (!paths.some(function(line) { return line && line.startsWith('/'); })) {
return _('You must provide at least one valid path (not a comment or empty line).');
}
return true;
};
o.cfgvalue = function(section_id) {
var v = uci.get('mergerfs', section_id, 'paths');
return clean_paths(v);
};
o.write = function(section_id, formvalue) {
return uci.set('mergerfs', section_id, 'paths', clean_paths(formvalue));
};
// type: select, name: createpolicy
o = s.option(form.ListValue, 'createpolicy', _('Create policy'), _('Policy for creating new files'));
o.editable = true;
o.value('epall', _('Existing path - all'));
o.value('epff', _('Existing path - first found'));
o.value('eplfs', _('Existing path - least free space'));
o.value('eplus', _('Existing path - least used space'));
o.value('epmfs', _('Existing path - most free space'));
o.value('eppfrd', _('Existing path - percentage free random distribution'));
o.value('eprand', _('Existing path - random'));
o.value('erofs', _('Read-only'));
o.value('ff', _('First found'));
o.value('lfs', _('Least free space'));
o.value('lus', _('Least used space'));
o.value('mfs', _('Most free space'));
o.value('msplfs', _('Most shared path - least free space'));
o.value('msplus', _('Most shared path - least used space'));
o.value('mspmfs', _('Most shared path - most free space'));
o.value('msppfrd', _('Most shared path - percentage free random distribution'));
o.value('newest', _('Newest file'));
o.value('pfrd', _('Percentage free random distribution'));
o.value('rand', _('Random'));
o.default = 'epmfs';
var parse_minfreespace = function(value) {
if (!value || typeof value !== 'string') return '';
// 去除所有空格
value = value.replace(/\s+/g, '');
// 转大写
value = value.toUpperCase();
// 去除尾部B
if (value.endsWith('B')) value = value.slice(0, -1);
if (value == '') return value;
// 保证有数字+单位,且单位为K/M/G之一
var m = value.match(/^(\d+)([KMG])$/);
if (m) {
return m[1] + m[2];
}
// 如果用户只输入了数字,默认加G
m = value.match(/^(\d+)$/);
if (m) {
return m[1] + 'G';
}
// 否则返回原值
return value;
};
// type: numberInput, name: minfreespace
o = s.option(form.Value, 'minfreespace', _('Minimum free space'));
o.placeholder = '4G';
o.editable = true;
o.validate = function(section_id, value) {
var parsed = parse_minfreespace(value);
if (parsed == '' || /^\d+[KMG]$/.test(parsed)) {
return true;
}
return _('Please enter a value like 4G, 500M, 100K');
};
o.cfgvalue = function(section_id) {
var v = uci.get('mergerfs', section_id, 'minfreespace');
return parse_minfreespace(v);
};
o.write = function(section_id, formvalue) {
return uci.set('mergerfs', section_id, 'minfreespace', parse_minfreespace(formvalue));
};
// type: textInput, name: options
o = s.option(form.Value, 'options', _('Options'));
o.modalonly = true;
o.placeholder = 'defaults,cache.files=off';
//o.default = 'defaults,cache.files=off';
return m.render();
}
});
+1
View File
@@ -0,0 +1 @@
zh_Hans
+123
View File
@@ -0,0 +1,123 @@
msgid ""
msgstr ""
"Language: zh_Hans\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
msgid "Create policy"
msgstr "创建策略"
msgid "Each line must be a path starting with a '/' or be a comment starting with '#'."
msgstr "每行必须是以'/'开头的路径或者以'#'开头的注释。"
msgid "Existing path - all"
msgstr "现有路径 - 全部"
msgid "Existing path - first found"
msgstr "现有路径 - 第一个找到的"
msgid "Existing path - least free space"
msgstr "现有路径 - 最少可用空间"
msgid "Existing path - least used space"
msgstr "现有路径 - 最少已用空间"
msgid "Existing path - most free space"
msgstr "现有路径 - 最多可用空间"
msgid "Existing path - percentage free random distribution"
msgstr "现有路径 - 按可用空间百分比随机分布"
msgid "Existing path - random"
msgstr "现有路径 - 随机"
msgid "Failed to unmount"
msgstr "卸载失败"
msgid "First found"
msgstr "第一个找到的"
msgid "If mounting fails, search \"mergerfs\" in the <a href=\"/cgi-bin/luci/admin/status/logs\">system log</a> for more information."
msgstr "如果挂载失败,在<a href=\"/cgi-bin/luci/admin/status/logs\">系统日志</a>中搜索\"mergerfs\"以获取信息。"
msgid "Least free space"
msgstr "最少可用空间"
msgid "Least used space"
msgstr "最少已用空间"
msgid "List of paths to merge, one per line. Line starting with '#' will be treated as a comment.<br>All paths must existed before mounting or the mounting will fail."
msgstr "要合并的路径列表,每行一个路径。以'#'开头的行将被视为注释。<br>挂载前所有路径必须存在,否则挂载将失败。"
msgid "MergerFS Pool"
msgstr "MergerFS 存储池"
msgid "MergerFS is a union filesystem, which allows you to pool multiple directories into a single mount point.<br>This is useful for combining storage space and files from multiple devices or directories into one accessible location.<br>New files can be distributed across the pooled directories based on various policies, such as most free space or first found.<br>More information can be found at <a target=\"_blank\" href=\"https://github.com/trapexit/mergerfs\">GitHub</a>."
msgstr "MergerFS 是一个联合文件系统,允许您将多个目录池化到一个挂载点。<br>这对于将来自多个设备或目录的存储空间和文件合并到一个可访问的位置很有用。<br>新文件可以根据各种策略(如最多可用空间或第一个找到的)分布在池化的目录中。<br>更多信息可以在 <a target=\"_blank\" href=\"https://github.com/trapexit/mergerfs\">GitHub</a> 找到。"
msgid "Minimum free space"
msgstr "最小可用空间"
msgid "Most free space"
msgstr "最多可用空间"
msgid "Most shared path - least free space"
msgstr "最多共享路径 - 最少可用空间"
msgid "Most shared path - least used space"
msgstr "最多共享路径 - 最少已用空间"
msgid "Most shared path - most free space"
msgstr "最多共享路径 - 最多可用空间"
msgid "Most shared path - percentage free random distribution"
msgstr "最多共享路径 - 按可用空间百分比随机分布"
msgid "Mount Configured MergerFS"
msgstr "挂载已配置的 MergerFS"
msgid "Mount to this path"
msgstr "挂载到此路径"
msgid "Mounted MergerFS"
msgstr "已挂载的 MergerFS"
msgid "No mounted MergerFS found"
msgstr "未找到已挂载的 MergerFS"
msgid "Mount Point is required, must start with \"/\" and cannot be root \"/\""
msgstr "挂载点是必需的,必须以\"/\"开头且不能是根目录\"/\""
msgid "Newest file"
msgstr "最新文件"
msgid "Options"
msgstr "选项"
msgid "Paths"
msgstr "路径"
msgid "Percentage free random distribution"
msgstr "按可用空间百分比随机分布"
msgid "Please enter a value like 4G, 500M, 100K"
msgstr "请输入类似 4G, 500M, 100K 的值"
msgid "Policy for creating new files"
msgstr "创建新文件的策略"
msgid "Pool Settings"
msgstr "存储池设置"
msgid "Put each folder on a new line."
msgstr "每行放一个文件夹。"
msgid "Random"
msgstr "随机"
msgid "Read-only"
msgstr "只读"
msgid "You must provide at least one valid path (not a comment or empty line)."
msgstr "您必须提供至少一个有效路径(不是注释或空行)。"
@@ -0,0 +1,13 @@
{
"admin/nas/mergerfs": {
"title": "MergerFS",
"action": {
"type": "view",
"path": "mergerfs"
},
"depends": {
"acl": [ "luci-app-mergerfs" ],
"uci": { "mergerfs": true }
}
}
}
@@ -0,0 +1,18 @@
{
"luci-app-mergerfs": {
"description": "Grant access to LuCI app mergerfs",
"read": {
"ubus": {
"luci": [ "getMountPoints" ]
},
"uci": [ "mergerfs" ]
},
"write": {
"file": {
"/bin/umount": [ "exec" ],
"/etc/init.d/mergerfs reload": [ "exec" ]
},
"uci": [ "mergerfs" ]
}
}
}
@@ -0,0 +1,4 @@
{
"config": "mergerfs",
"init": "mergerfs"
}