update 2025-12-21 20:45:42

This commit is contained in:
kenzok8
2025-12-21 20:45:42 +08:00
parent 9c541ee081
commit 4feac3547d
21 changed files with 2386 additions and 1381 deletions

View File

@@ -7,15 +7,15 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-partexp
PKG_VERSION:=1.3.2
PKG_RELEASE:=20250706
LUCI_TITLE:=LuCI Support for Automatic Partition Mount
LUCI_PKGARCH:=all
LUCI_DEPENDS:=+fdisk +block-mount +bc +blkid +parted +btrfs-progs +losetup +resize2fs +e2fsprogs +f2fs-tools +kmod-loop
PKG_VERSION:=2.0.2
PKG_RELEASE:=20251221
PKG_LICENSE:=Apache-2.0
PKG_MAINTAINER:=Sirpdboy <herboy2008@gmail.com>
LUCI_TITLE:=LuCI Support for Automatic Partition Mount
LUCI_DEPENDS:=+fdisk +block-mount +bc +blkid +parted +btrfs-progs +losetup +resize2fs +e2fsprogs +f2fs-tools +kmod-loop
LUCI_PKGARCH:=all
include $(TOPDIR)/feeds/luci/luci.mk

View File

@@ -1,127 +0,0 @@
### 访问数:[![](https://visitor-badge.glitch.me/badge?page_id=sirpdboy-visitor-badge)] [![](https://img.shields.io/badge/TG群-点击加入-FFFFFF.svg)](https://t.me/joinchat/AAAAAEpRF88NfOK5vBXGBQ)
![screenshots](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/说明1.jpg)
=
# luci-app-partexp
luci-app-partexp 一键自动格式化分区、扩容、自动挂载插件
[![若部分图片无法正常显示,请挂上机场浏览或点这里到末尾看修复教程](https://visitor-badge.glitch.me/badge?page_id=sirpdboy-visitor-badge)](#解决-github-网页上图片显示失败的问题) [![](https://img.shields.io/badge/TG群-点击加入-FFFFFF.svg)](https://t.me/joinchat/AAAAAEpRF88NfOK5vBXGBQ)
[luci-app-partexp](https://github.com/sirpdboy/luci-app-partexp)
======================
**认真阅读完毕** 本页面,本页面包含注意事项和如何使用。
## 功能说明:
#### 一键自动格式化分区、扩容、自动挂载插件专为OPENWRT设计简化OPENWRT在分区挂载上烦锁的操作。本插件是sirpdboy耗费大量精力制作测试请勿删除制作者信息
<!-- TOC -->
- [partexp](#luci-app-partexp)
- [特性](#特性)
- [使用方法](#使用方法)
- [说明](#说明)
- [界面](#界面)
- [捐助](#捐助)
<!-- /TOC -->
## 版本
- 最新更新版本号: V1.3.1
- 更新日期2025年3月26日
- 更新内容:
- 重新整理分区扩容代码,解决一些不合理的地方。
- 加入对目标分区的格式可以指定格式化为ext4,ntfs和Btrfs以及不格式化。
- 当做为根目录 /或者 /overlay时密然会格式化为ext4格式。
- 目前在X86的机器上测试完全正常其它路由设备上未测试。有问题请提交硬盘分区情况和错误提示。
## 特性
luci-app-partexp 自动获格式化分区扩容,自动挂载插件
## 使用方法
- 将luci-app-partexp添加至 LEDE/OpenWRT 源码的方法。
### 下载源码方法:
```Brach
# 下载源码
git clone https://github.com/sirpdboy/luci-app-partexp.git package/luci-app-partexp
make menuconfig
```
### 配置菜单
```Brach
make menuconfig
# 找到 LuCI -> Applications, 选择 luci-app-partexp, 保存后退出。
```
### 编译
```Brach
# 编译固件
make package/luci-app-partexp/compile V=s
```
## 说明
![screenshots](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/说明2.jpg)
## 界面
![screenshots](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/partexp.png)
## 使用与授权相关说明
- 本人开源的所有源码,任何引用需注明本处出处,如需修改二次发布必告之本人,未经许可不得做于任何商用用途。
# My other project
- 网络速度测试 https://github.com/sirpdboy/NetSpeedTest
- 定时设置插件 : https://github.com/sirpdboy/luci-app-autotimeset
- 关机功能插件 : https://github.com/sirpdboy/luci-app-poweroffdevice
- opentopd主题 : https://github.com/sirpdboy/luci-theme-opentopd
- kucat 主题: https://github.com/sirpdboy/luci-theme-kucat
- 家长控制: https://github.com/sirpdboy/luci-theme-parentcontrol
- 系统高级设置 : https://github.com/sirpdboy/luci-app-advanced
- ddns-go动态域名: https://github.com/sirpdboy/luci-app-ddns-go
- 进阶设置(系统高级设置+主题设置kucat/agron/opentopd: https://github.com/sirpdboy/luci-app-advancedplus
- 设置向导: https://github.com/sirpdboy/luci-app-wizard
- 分区扩容: https://github.com/sirpdboy/luci-app-partexp
- lukcy大吉: https://github.com/sirpdboy/luci-app-lukcy
## 捐助
![screenshots](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/说明3.jpg)
| <img src="https://img.shields.io/badge/-支付宝-F5F5F5.svg" href="#赞助支持本项目-" height="25" alt="图飞了😂"/> | <img src="https://img.shields.io/badge/-微信-F5F5F5.svg" height="25" alt="图飞了😂" href="#赞助支持本项目-"/> |
| :-----------------: | :-------------: |
|![xm1](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/支付宝.png) | ![xm1](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/微信.png) |
<a href="#readme">
<img src="https://img.shields.io/badge/-返回顶部-orange.svg" alt="图飞了😂" title="返回顶部" align="right"/>
</a>

View File

@@ -0,0 +1,963 @@
/*
* Copyright (C) 2022-2025 Sirpdboy <herboy2008@gmail.com>
*
* Licensed to the public under the Apache License 2.0
*/
'use strict';
'require form';
'require fs';
'require rpc';
'require uci';
'require ui';
'require view';
// 声明 RPC 接口
var callPartExpAutopart = rpc.declare({
object: 'partexp',
method: 'autopart'
});
var callPartExpGetLog = rpc.declare({
object: 'partexp',
method: 'get_log',
params: ['position']
});
var callPartExpGetDevices = rpc.declare({
object: 'partexp',
method: 'get_devices'
});
var callPartExpGetStatus = rpc.declare({
object: 'partexp',
method: 'get_status'
});
// 添加保存配置的 RPC 声明
var callPartExpSaveConfig = rpc.declare({
object: 'partexp',
method: 'save_config',
params: ['target_function', 'target_disk', 'keep_config', 'format_type']
});
return view.extend({
load: function() {
return Promise.all([
L.resolveDefault(fs.stat('/usr/bin/partexp'), null),
L.resolveDefault(fs.stat('/tmp/partexp.log'), null)
]);
},
render: function(data) {
var container = E('div', { class: 'cbi-map' });
var htmlParts = [
'<style>',
'.state-ctl .state { display: none !important; }',
'.state-ctl.state-ctl-ready .state.state-ready,',
'.state-ctl.state-ctl-executing .state.state-executing {',
' display: block !important;',
'}',
'.progress-container {',
' width: 100%;',
' height: 20px;',
' background: rgba(0,0,0,0.2);',
' border-radius: 10px;',
' display: inline-block;',
' margin: 10px 0;',
' vertical-align: middle;',
' position: relative;',
' overflow: hidden;',
'}',
'.progress-bar {',
' height: 100%;',
' background: linear-gradient(90deg, #4CAF50, #8BC34A);',
' transition: width 0.3s ease-out;',
' position: absolute;',
' left: 0;',
' top: 0;',
'}',
'.progress-text {',
' position: absolute;',
' width: 100%;',
' text-align: center;',
' line-height: 20px;',
' font-size: 12px;',
' font-weight: bold;',
' color: #eee;',
' z-index: 1;',
' text-shadow: 1px 1px 2px rgba(0,0,0,0.5);',
'}',
'.error-message {',
' color: #dc3545;',
'}',
'.info-note {',
' padding: 10px;',
' margin: 10px 0;',
' border-radius: 4px;',
'}',
'.log-view {',
' font-family: "Courier New", monospace;',
' font-size: 12px;',
' height: 300px;',
' overflow-y: auto;',
' white-space: pre-wrap;',
'}',
'</style>',
'<h2 name="content">' + _('One click partition expansion mounting tool') + '</h2>',
'<div class="cbi-section-descr">',
' <div class="info-note">',
' ' + _('Automatically format and mount the target device partition. If there are multiple partitions, it is recommended to manually delete all partitions before using this tool.') + '<br>',
' ' + _('For specific usage, see:') + ' ',
' <a href="https://github.com/sirpdboy/luci-app-partexp.git" target="_blank">',
' GitHub @partexp',
' </a>',
' </div>',
'</div>',
'<div class="state-ctl state-ctl-ready" id="state-container">',
' <div class="cbi-section cbi-section-node">',
' <div class="state state-ready">',
' <form id="partexp-form">',
' <div class="cbi-value">',
' <label class="cbi-value-title" for="target_function">' + _('Select function') + '</label>',
' <div class="cbi-value-field">',
' <select class="cbi-input-select" id="target_function" name="target_function">',
' <option value="/">' + _('Used to extend to the root directory of EXT4 firmware(Ext4 /)') + '</option>',
' <option value="/overlay">' + _('Expand application space overlay (/overlay)') + '</option>',
' <option value="/opt">' + _('Used as Docker data disk (/opt)') + '</option>',
' <option value="/mnt">' + _('Normal mount and use by device name(/mnt/x1)') + '</option>',
' </select>',
' <div class="cbi-value-description">' + _('Select the function to be performed') + '</div>',
' </div>',
' </div>',
' <div class="cbi-value">',
' <label class="cbi-value-title" for="target_disk">' + _('Destination hard disk') + '</label>',
' <div class="cbi-value-field">',
' <select class="cbi-input-select" id="target_disk" name="target_disk">',
' <option value="">' + _('Loading devices...') + '</option>',
' </select>',
' <div class="cbi-value-description">' + _('Select the hard disk device to operate') + '</div>',
' </div>',
' </div>',
' <div class="cbi-value">',
' <label class="cbi-value-title" for="keep_config">' + _('Keep configuration') + '</label>',
' <div class="cbi-value-field">',
' <input type="checkbox" class="cbi-input-checkbox" id="keep_config" name="keep_config" value="1" />',
' <label for="keep_config">' + _('Tick means to retain the settings') + '</label>',
' </div>',
' </div>',
' <div class="cbi-value">',
' <label class="cbi-value-title" for="format_type">' + _('Format system type') + '</label>',
' <div class="cbi-value-field">',
' <select class="cbi-input-select" id="format_type" name="format_type">',
' <option value="0">' + _('No formatting required') + '</option>',
' <option value="ext4">' + _('Linux system partition(EXT4)') + '</option>',
' <option value="btrfs">' + _('Large capacity storage devices(Btrfs)') + '</option>',
' <option value="ntfs">' + _('Windows system partition(NTFS)') + '</option>',
' </select>',
' </div>',
' </div>',
' <div class="cbi-value cbi-value-last">',
' <label class="cbi-value-title">' + _('Perform operation') + '</label>',
' <div class="cbi-value-field">',
' <button type="button" class="cbi-button cbi-button-apply" id="execute-btn">',
' ' + _('Click to execute') + '',
' </button>',
' </div>',
' </div>',
' </form>',
' </div>',
' <div class="state state-executing">',
' <div class="cbi-value">',
' <label class="cbi-value-title" id="execute_status">' + _('Starting operation...') + '</label>',
' <div class="cbi-value-field">',
' <div class="progress-container">',
' <div id="progress-bar" class="progress-bar" style="width: 0%"></div>',
' <div id="progress-text" class="progress-text">0%</div>',
' </div>',
' </div>',
' </div>',
' </div>',
' </div>',
' <div id="log-section" style="display: block; margin-top: 20px;">',
' <div class="cbi-value">',
' <label class="cbi-value-title">' + _('Operation Log') + '</label>',
' <div class="cbi-value-field">',
' <textarea id="log-view" class="log-view" readonly="readonly" rows="15"></textarea>',
' </div>',
' </div>',
' </div>',
'</div>'
];
container.innerHTML = htmlParts.join('');
var self = this;
// uci 对象已经在全局作用域可用
var uci = self.uci || window.uci;
setTimeout(function() {
self.initDOM();
self.bindEvents();
self.loadDevices();
self.loadSavedConfig();
self.checkOperationStatus();
self.loadExistingLog();
}, 100);
return container;
},
initDOM: function() {
this.dom = {
stateContainer: document.querySelector('#state-container'),
targetFunction: document.querySelector('#target_function'),
targetDisk: document.querySelector('#target_disk'),
keepConfig: document.querySelector('#keep_config'),
formatType: document.querySelector('#format_type'),
executeBtn: document.querySelector('#execute-btn'),
logView: document.querySelector('#log-view'),
progressBar: document.querySelector('#progress-bar'),
progressText: document.querySelector('#progress-text'),
executeStatus: document.querySelector('#execute_status')
};
// 初始化状态变量
this.logPosition = '0';
this.logPolling = null;
this.isRunning = false;
this.operationComplete = false;
this.pollErrorCount = 0;
this.pollingStartTime = 0;
this.lastPollTime = 0;
this.currentProgress = 0;
this.autoSaveTimer = null;
this.isNewOperation = false; // 标记是否是新操作
},
bindEvents: function() {
var self = this;
if (this.dom.executeBtn) {
this.dom.executeBtn.addEventListener('click', function(e) {
e.preventDefault();
self.executeOperation();
});
}
// 表单变化事件 - 自动保存
[this.dom.targetFunction, this.dom.targetDisk, this.dom.formatType].forEach(function(element) {
if (element) {
element.addEventListener('change', function() {
self.autoSaveConfig();
self.updateFormVisibility();
});
}
});
// 复选框特殊处理
if (this.dom.keepConfig) {
this.dom.keepConfig.addEventListener('click', function() {
self.autoSaveConfig();
});
}
// 初始化表单可见性
if (this.dom.targetFunction) {
this.updateFormVisibility();
}
},
// 加载设备列表
loadDevices: function() {
var self = this;
callPartExpGetDevices().then(function(response) {
if (!response || !response.devices || response.devices.length === 0) {
return;
}
// 清空设备列表
if (self.dom.targetDisk) {
self.dom.targetDisk.innerHTML = '';
// 添加设备选项
response.devices.forEach(function(device) {
var option = document.createElement('option');
option.value = device.name;
option.textContent = device.name + ' (' + device.dev + ', ' + device.size + ' MB)';
self.dom.targetDisk.appendChild(option);
});
}
}).catch(function(error) {
console.error('Failed to load devices:', error);
});
},
// 加载现有的日志文件内容
loadExistingLog: function() {
var self = this;
// 初始化时获取现有日志内容
callPartExpGetLog('0').then(function(response) {
if (response && response.log) {
var logContent = response.log.toString().trim();
if (logContent && self.dom.logView) {
// 显示现有日志内容
self.dom.logView.value = logContent;
// 自动滚动到底部
setTimeout(function() {
if (self.dom.logView && self.dom.logView.value) {
self.dom.logView.scrollTop = self.dom.logView.scrollHeight;
}
}, 100);
// 更新日志位置
if (response.position) {
self.logPosition = response.position;
}
if (!self.isRunning && logContent.includes('正在执行') && !logContent.includes('操作完成')) {
self.isRunning = true;
self.switchState('executing');
self.startLogPolling();
}
}
}
}).catch(function(error) {
console.error('Failed to load existing log:', error);
});
},
loadSavedConfig: function() {
var self = this;
return fs.read('/etc/config/partexp').then(function(content) {
if (!content) {
self.setDefaultConfig();
return;
}
// 解析配置文件
var lines = content.split('\n');
var config = {};
lines.forEach(function(line) {
line = line.trim();
if (line.startsWith('option')) {
var parts = line.split(/\s+/);
if (parts.length >= 3) {
var key = parts[1];
var value = parts.slice(2).join(' ').replace(/^['"]|['"]$/g, '');
config[key] = value;
}
}
});
// 设置表单值
if (self.dom.targetFunction) {
self.dom.targetFunction.value = config.target_function || '/opt';
}
if (self.dom.targetDisk && config.target_disk) {
// 等待设备加载完成后设置
setTimeout(function() {
if (self.dom.targetDisk) {
self.dom.targetDisk.value = config.target_disk;
}
}, 500);
}
if (self.dom.keepConfig) {
self.dom.keepConfig.checked = (config.keep_config === '1');
}
if (self.dom.formatType) {
self.dom.formatType.value = config.format_type || '0';
}
// 更新配置缓存
self.configCache = config;
// 更新表单可见性
self.updateFormVisibility();
}).catch(function(error) {
console.log('Failed to load config:', error);
self.setDefaultConfig();
});
},
// 设置默认配置
setDefaultConfig: function() {
if (this.dom.targetFunction) {
this.dom.targetFunction.value = '/opt';
}
if (this.dom.formatType) {
this.dom.formatType.value = '0';
}
if (this.dom.keepConfig) {
this.dom.keepConfig.checked = false;
}
this.updateFormVisibility();
this.configCache = {
target_function: '/opt',
target_disk: '',
keep_config: '0',
format_type: '0'
};
},
// 自动保存配置(防抖处理)
autoSaveConfig: function() {
var self = this;
// 清除之前的定时器
if (this.autoSaveTimer) {
clearTimeout(this.autoSaveTimer);
}
// 设置新的定时器1.5秒后保存
this.autoSaveTimer = setTimeout(function() {
self.saveCurrentConfig();
}, 1500);
},
// 保存当前配置
saveCurrentConfig: function() {
var self = this;
// 获取当前表单值
var targetFunction = this.dom.targetFunction ? this.dom.targetFunction.value : '/opt';
var targetDisk = this.dom.targetDisk ? this.dom.targetDisk.value : '';
var keepConfig = this.dom.keepConfig ? this.dom.keepConfig.checked : false;
var formatType = this.dom.formatType ? this.dom.formatType.value : '0';
if (callPartExpSaveConfig) {
return callPartExpSaveConfig(
targetFunction,
targetDisk,
keepConfig ? '1' : '0',
formatType
).then(function(response) {
if (response && response.success) {
self.configCache = {
target_function: targetFunction,
target_disk: targetDisk,
keep_config: keepConfig ? '1' : '0',
format_type: formatType
};
return true;
} else {
console.warn('RPC save failed, falling back to file write');
return self.saveConfigToFile(targetFunction, targetDisk, keepConfig, formatType);
}
}).catch(function(error) {
console.error('RPC save config error:', error);
return self.saveConfigToFile(targetFunction, targetDisk, keepConfig, formatType);
});
} else {
// 如果 RPC 不可用,直接使用文件写入
return self.saveConfigToFile(targetFunction, targetDisk, keepConfig, formatType);
}
},
// 备选方案:直接写入配置文件
saveConfigToFile: function(targetFunction, targetDisk, keepConfig, formatType) {
var configContent = [
'# Auto-generated by partexp',
'',
'config global global',
"\toption target_function '" + targetFunction + "'",
"\toption target_disk '" + targetDisk + "'",
"\toption keep_config '" + (keepConfig ? '1' : '0') + "'",
"\toption format_type '" + formatType + "'",
''
].join('\n');
return fs.write('/etc/config/partexp', configContent).then(function() {
console.log('Settings saved to file /etc/config/partexp');
return true;
}).catch(function(error) {
console.error('Failed to save settings to file:', error);
return false;
});
},
// 执行操作
executeOperation: function() {
var self = this;
// 先保存配置
this.saveCurrentConfig();
var target_function = this.dom.targetFunction.value;
var target_disk = this.dom.targetDisk.value;
if (target_function !== '/' && (!target_disk || target_disk.trim() === '')) {
alert(_('Please select a target disk'));
return;
}
// 确认操作
var confirmMessage = _('Are you sure you want to execute partition expansion?') + '\n\n' +
_('Function:') + ' ' + this.getFunctionDescription(target_function) + '\n' +
(target_function !== '/' ? _('Disk:') + ' ' + target_disk + '\n' : '') +
(target_function === '/' || target_function === '/overlay' ?
_('Keep config:') + ' ' + (this.dom.keepConfig.checked ? _('Yes') : _('No')) + '\n' : '') +
(target_function === '/opt' || target_function === '/dev' ?
_('Format type:') + ' ' + this.getFormatTypeDescription(this.dom.formatType.value) + '\n' : '') +
'\n' + _('This operation may take several minutes.');
if (!confirm(confirmMessage)) {
return;
}
// 重置操作状态
this.resetOperationState();
// 标记为新操作开始
this.isNewOperation = true;
if (this.dom.logView) {
this.dom.logView.value = _('正在启动操作...');
}
// 更新按钮状态
if (this.dom.executeBtn) {
this.dom.executeBtn.disabled = true;
this.dom.executeBtn.textContent = _('Executing...');
}
// 切换到执行状态
this.switchState('executing');
// 开始进度显示
this.updateProgress(5, _('Starting operation...'));
// 调用分区操作
callPartExpAutopart()
.then(function(response) {
if (response && response.success) {
// 操作开始成功
self.isRunning = true;
self.operationComplete = false;
self.startLogPolling();
if (self.dom.executeStatus) {
self.dom.executeStatus.textContent = _('Operation started successfully');
}
} else {
// 操作启动失败
var errorMsg = response && response.message ? response.message : _('Operation failed');
self.handleOperationError(errorMsg);
}
})
.catch(function(error) {
console.error('Operation failed:', error);
self.handleOperationError(_('Failed to start operation:') + ' ' + (error.message || _('Unknown error')));
});
},
// 重置操作状态
resetOperationState: function() {
this.logPosition = '0';
this.isRunning = true;
this.operationComplete = false;
this.pollErrorCount = 0;
this.pollingStartTime = Date.now();
this.lastPollTime = 0;
this.currentProgress = 0;
// 重置进度条
this.updateProgress(0, _('Starting operation...'));
},
// 处理操作错误
handleOperationError: function(errorMsg) {
alert(errorMsg);
if (this.dom.executeBtn) {
this.dom.executeBtn.disabled = false;
this.dom.executeBtn.textContent = _('Click to execute');
}
this.switchState('ready');
this.stopLogPolling();
// 在日志中显示错误信息
if (this.dom.logView) {
var currentLog = this.dom.logView.value || '';
this.dom.logView.value = currentLog + '\n\n' + _('操作失败:') + ' ' + errorMsg;
setTimeout(() => {
if (this.dom.logView) {
this.dom.logView.scrollTop = this.dom.logView.scrollHeight;
}
}, 100);
}
},
// 更新表单可见性
updateFormVisibility: function() {
if (!this.dom.targetFunction || !this.dom.targetDisk ||
!this.dom.keepConfig || !this.dom.formatType) return;
var func = this.dom.targetFunction.value;
var diskDiv = this.dom.targetDisk.closest('.cbi-value');
var keepDiv = this.dom.keepConfig.closest('.cbi-value');
var formatDiv = this.dom.formatType.closest('.cbi-value');
if (!diskDiv || !keepDiv || !formatDiv) return;
if (func === '/') {
diskDiv.style.display = 'none';
formatDiv.style.display = 'none';
keepDiv.style.display = 'block';
} else if (func === '/overlay') {
diskDiv.style.display = 'block';
formatDiv.style.display = 'none';
keepDiv.style.display = 'block';
} else {
diskDiv.style.display = 'block';
formatDiv.style.display = 'block';
keepDiv.style.display = 'none';
}
},
// 检查操作状态
checkOperationStatus: function() {
var self = this;
callPartExpGetStatus().then(function(response) {
if (response && response.running) {
// 有操作在进行中
self.isRunning = true;
self.switchState('executing');
self.startLogPolling();
// 禁用执行按钮
if (self.dom.executeBtn) {
self.dom.executeBtn.disabled = true;
self.dom.executeBtn.textContent = _('Operation in progress...');
}
// 更新状态
if (self.dom.executeStatus) {
self.dom.executeStatus.textContent = _('Operation in progress...');
}
}
}).catch(function(error) {
console.error('Failed to check operation status:', error);
});
},
// 开始轮询日志
startLogPolling: function() {
var self = this;
// 停止现有的轮询
this.stopLogPolling();
// 重置状态
this.pollErrorCount = 0;
this.pollingStartTime = Date.now();
this.lastPollTime = 0;
// 更新进度显示
this.updateProgress(10, _('Operation in progress...'));
// 开始轮询
this.logPolling = setInterval(function() {
// 检查是否超时20分钟超时
if (Date.now() - self.pollingStartTime > 20 * 60 * 1000) {
console.error('Operation timeout');
self.stopLogPolling();
self.isRunning = false;
// 显示超时信息
if (self.dom.logView) {
var currentLog = self.dom.logView.value || '';
self.dom.logView.value = currentLog + '\n\n[超时] 操作超过20分钟未完成请检查系统';
setTimeout(() => {
if (self.dom.logView) {
self.dom.logView.scrollTop = self.dom.logView.scrollHeight;
}
}, 100);
}
self.switchState('ready');
if (self.dom.executeBtn) {
self.dom.executeBtn.disabled = false;
self.dom.executeBtn.textContent = _('Click to execute');
}
return;
}
self.pollLog();
}, 3000); // 每3秒轮询一次减少频率
},
pollLog: function() {
var self = this;
if (!this.isRunning) {
this.stopLogPolling();
return;
}
var pollStartTime = Date.now();
// 总是从位置0开始获取完整日志内容
callPartExpGetLog('0').then(function(response) {
if (!response) {
console.error('No response from log polling');
return;
}
if (pollStartTime < self.lastPollTime) {
return;
}
self.lastPollTime = pollStartTime;
// 处理日志内容
if (response.log !== undefined) {
var logContent = response.log.toString().trim();
if (response.position) {
self.logPosition = response.position;
}
if (self.dom.logView) {
if (logContent !== '') {
self.dom.logView.value = logContent;
// 自动滚动到底部
setTimeout(function() {
if (self.dom.logView && self.dom.logView.value) {
self.dom.logView.scrollTop = self.dom.logView.scrollHeight;
}
}, 50);
}
}
// 更新进度
self.parseAndUpdateProgress(logContent);
// 检查操作是否完成
if (self.checkOperationComplete(logContent)) {
self.handleOperationComplete();
}
}
// 检查RPC返回的完成状态
if (response.complete) {
self.handleOperationComplete();
}
// 重置错误计数
self.pollErrorCount = 0;
}).catch(function(error) {
console.error('Log polling error:', error);
// 如果多次失败,停止轮询
self.pollErrorCount = (self.pollErrorCount || 0) + 1;
if (self.pollErrorCount > 5) {
console.error('Too many polling errors, stopping');
self.stopLogPolling();
self.isRunning = false;
self.switchState('ready');
if (self.dom.executeBtn) {
self.dom.executeBtn.disabled = false;
self.dom.executeBtn.textContent = _('Click to execute');
}
// 显示错误信息
if (self.dom.logView) {
var currentLog = self.dom.logView.value || '';
self.dom.logView.value = currentLog + '\n\n[错误] 日志轮询失败,请刷新页面查看最新状态';
setTimeout(() => {
if (self.dom.logView) {
self.dom.logView.scrollTop = self.dom.logView.scrollHeight;
}
}, 100);
}
}
});
},
// 检查操作是否完成
checkOperationComplete: function(logText) {
if (!logText) return false;
// 检查日志中是否包含操作完成标记
var completeMarkers = [
'重启设备',
'操作完成'
];
for (var i = 0; i < completeMarkers.length; i++) {
if (logText.includes(completeMarkers[i])) {
return true;
}
}
return false;
},
// 处理操作完成
handleOperationComplete: function() {
if (this.operationComplete) {
return;
}
this.operationComplete = true;
this.isRunning = false;
this.isNewOperation = false;
// 立即停止轮询
this.stopLogPolling();
if (this.dom.logView) {
var currentLog = this.dom.logView.value || '';
if (!currentLog.includes('操作完成')) {
this.dom.logView.value = currentLog;
setTimeout(() => {
if (this.dom.logView) {
this.dom.logView.scrollTop = this.dom.logView.scrollHeight;
}
}, 100);
}
}
// 进度条显示100%
this.updateProgress(100, _('Operation completed'));
// 启用执行按钮
setTimeout(() => {
if (this.dom.executeBtn) {
this.dom.executeBtn.disabled = false;
this.dom.executeBtn.textContent = _('Click to execute');
}
// 切换回就绪状态
setTimeout(() => {
this.switchState('ready');
}, 3000);
}, 2000);
},
// 解析并更新进度
parseAndUpdateProgress: function(logText) {
if (!logText || !this.dom.executeStatus) return;
// 尝试从日志中提取进度信息
var percent = 0;
var statusMessage = _('Operation in progress...');
if (logText.includes('100%') || logText.includes('操作完成') || logText.includes('扩容成功')) {
percent = 100;
statusMessage = _('Operation completed');
} else if ( logText.includes('错误') || logText.includes('error')) {
// 错误情况,不更新进度
return;
} else if (logText.includes('分区扩容和挂载到') || logText.includes('正在挂载')) {
percent = 90;
statusMessage = _('Getting device information');
} else if (logText.includes('检测设备')) {
percent = 60;
statusMessage = _('Checking partition format');
} else if (logText.includes('开始检测目标')) {
percent = 50;
statusMessage = _('Checking target device');
} else if (logText.includes('定位到操作目标设备分区')) {
percent = 40;
statusMessage = _('Locating target partition');
} else if (logText.includes('目标盘') && logText.includes('有剩余空间')) {
percent = 30;
statusMessage = _('Checking free space');
} else if (logText.includes('操作功能')) {
percent = 20;
statusMessage = _('Starting operation');
} else if (logText.includes('开始执行') || logText.includes('Starting')) {
percent = 10;
statusMessage = _('Initializing...');
}
// 确保进度不会倒退
if (percent > 0) {
this.currentProgress = Math.max(this.currentProgress || 0, percent);
} else {
// 如果没有明确的进度标记,逐渐增加进度
this.currentProgress = Math.min(90, (this.currentProgress || 0) + 1);
}
// 更新进度显示
this.updateProgress(this.currentProgress, statusMessage);
},
// 更新进度显示
updateProgress: function(percent, message) {
if (!this.dom.progressBar || !this.dom.progressText || !this.dom.executeStatus) {
return;
}
// 确保百分比在有效范围内
percent = Math.max(0, Math.min(100, percent));
// 更新进度条
this.dom.progressBar.style.width = percent + '%';
// 更新进度文本
this.dom.progressText.textContent = percent + '%';
// 更新状态消息
this.dom.executeStatus.textContent = message;
},
// 停止轮询日志
stopLogPolling: function() {
if (this.logPolling) {
clearInterval(this.logPolling);
this.logPolling = null;
}
},
// 切换状态
switchState: function(to) {
if (!this.dom.stateContainer) return;
// 移除所有状态类
this.dom.stateContainer.classList.remove(
'state-ctl-ready',
'state-ctl-executing'
);
// 添加新状态类
this.dom.stateContainer.classList.add('state-ctl-' + to);
},
// 获取功能描述
getFunctionDescription: function(func) {
switch(func) {
case '/': return _('Extend to root directory');
case '/overlay': return _('Expand overlay');
case '/opt': return _('Docker data disk');
case '/dev': return _('Normal mount');
default: return func;
}
},
// 获取格式化类型描述
getFormatTypeDescription: function(type) {
switch(type) {
case '0': return _('No formatting');
case 'ext4': return _('EXT4');
case 'btrfs': return _('Btrfs');
case 'ntfs': return _('NTFS');
default: return type;
}
},
// 页面生命周期方法
handleSaveApply: null,
handleSave: null,
handleReset: null
});

View File

@@ -1,50 +0,0 @@
--[[
LuCI - Lua Configuration Partition Expansion
Copyright (C) 2022-2025 sirpdboy <herboy2008@gmail.com> https://github.com/sirpdboy/partexp
]]--
local fs = require "nixio.fs"
local http = require "luci.http"
local uci = require"luci.model.uci".cursor()
local name = 'partexp'
module("luci.controller.partexp", package.seeall)
function index()
local e = entry({"admin","system","partexp"},alias("admin", "system", "partexp", "global"),_("Partition Expansion"), 54)
e.dependent = false
e.acl_depends = { "luci-app-partexp" }
entry({"admin","system","partexp","global"}, cbi('partexp/global', {hideapplybtn = true, hidesavebtn = true, hideresetbtn = true}), _('Partition Expansion'), 10).leaf = true
entry({"admin", "system", "partexp","partexprun"}, call("partexprun"))
entry({"admin", "system", "partexp", "check"}, call("act_check"))
end
function act_check()
http.prepare_content("text/plain; charset=utf-8")
local f=io.open("/tmp/partexp.log", "r+")
local fdp=fs.readfile("/tmp/lucilogpos") or 0
f:seek("set",fdp)
local a=f:read(2048000) or ""
fdp=f:seek()
fs.writefile("/tmp/lucilogpos",tostring(fdp))
f:close()
http.write(a)
end
function partexprun()
local kconfig = http.formvalue('kconfig')
local eformat = http.formvalue('eformat')
local targetf = http.formvalue('targetf')
local targetd = http.formvalue('targetd')
uci:set(name, 'global', 'target_disk', targetd)
uci:set(name, 'global', 'target_function', targetf)
uci:set(name, 'global', 'format_type', eformat)
uci:set(name, 'global', 'keep_config', kconfig)
uci:commit(name)
fs.writefile("/tmp/lucilogpos","0")
http.prepare_content("application/json")
http.write('')
luci.sys.exec("/etc/init.d/partexp autopart > /tmp/partexp.log 2>&1 &")
end

View File

@@ -1,79 +0,0 @@
--[[
LuCI - Lua Configuration Interface
Copyright (C) 2022-2025 sirpdboy <herboy2008@gmail.com> https://github.com/sirpdboy/luci-app-partexp
]]--
local fs = require "nixio.fs"
local util = require "nixio.util"
local tp = require "luci.template.parser"
local uci=luci.model.uci.cursor()
luci.sys.exec("echo '-' >/tmp/partexp.log&&echo 1 > /tmp/lucilogpos" )
local target_devnames = {}
for dev in fs.dir("/dev") do
if dev:match("^sd[a-z]$")
or dev:match("^mmcblk%d+$")
or dev:match("^sata[a-z]$")
or dev:match("^nvme%d+n%d+$")
or dev:match("^vd[a-z]")
then
table.insert(target_devnames, dev)
end
end
local devices = {}
for i, bname in pairs(target_devnames) do
local device_info = {}
local device = "/dev/" .. bname
device_info["name"] = bname
device_info["dev"] = device
s = tonumber((fs.readfile("/sys/class/block/%s/size" % bname)))
device_info["size"] = s and math.floor(s / 2048)
devices[#devices+1] = device_info
end
local m,t,e
m = Map("partexp", "<font color='green'>" .. translate("One click partition expansion mounting tool") .."</font>",
translate( "Automatically format and mount the target device partition. If there are multiple partitions, it is recommended to manually delete all partitions before using this tool.<br/>For specific usage, see:") ..translate("<a href=\'https://github.com/sirpdboy/luci-app-partexp.git' target=\'_blank\'>GitHub @sirpdboy:luci-app-partexp</a>") )
t=m:section(TypedSection,"global")
t.anonymous=true
e=t:option(ListValue,"target_function", translate("Select function"),translate("Select the function to be performed"))
e:value("/", translate("Used to extend to the root directory of EXT4 firmware(Ext4 /)"))
e:value("/overlay", translate("Expand application space overlay (/overlay)"))
e:value("/opt", translate("Used as Docker data disk (/opt)"))
e:value("/dev", translate("Normal mount and use by device name(/mnt/x1)"))
e.default="/opt"
e=t:option(ListValue,"target_disk", translate("Destination hard disk"),translate("Select the hard disk device to operate"))
e:depends("target_function", "/overlay")
e:depends("target_function", "/opt")
e:depends("target_function", "/dev")
for i, d in ipairs(devices) do
if d.name and d.size then
e:value(d.name, "%s (%s, %d MB)" %{ d.name, d.dev, d.size })
elseif d.name then
e:value(d.name, "%s (%s)" %{ d.name, d.dev })
end
end
e=t:option(Flag,"keep_config",translate("Keep configuration"),translate("Tick means to retain the settings"))
e:depends("target_function", "/overlay")
e:depends("target_function", "/")
e.default=0
e=t:option(ListValue,'format_type', translate('Format system type'))
e:depends("target_function", "/opt")
e:depends("target_function", "/dev")
e:value("0", translate("No formatting required"))
e:value("ext4", translate("Linux system partition(EXT4)"))
e:value("btrfs", translate("Large capacity storage devices(Btrfs)"))
e:value("ntfs", translate("Windows system partition(NTFS)"))
e.default="0"
e=t:option(Button, "restart", translate("Perform operation"))
e.inputtitle=translate("Click to execute")
e.rawhtml=true
e.template ='partexp'
return m

View File

@@ -1,130 +0,0 @@
<%#
Copyright (C) 2022-2024 sirpdboy <herboy2008@gmail.com> https://github.com/sirpdboy/partexp
-%>
<%+cbi/valueheader%>
<%local fs=require"nixio.fs"%>
<input type="button" class="btn cbi-button cbi-button-apply" id="apply_run_button" value="<%:Click to execute%>" onclick=" return apply_run(this) "/>
<div id="logview" style="display:none">
<input type="checkbox" id="reversetag" value="reverse" onclick=" return reverselog()" style="vertical-align:middle;height: auto;"><%:reverse%></input>
<textarea id="cbid.logview.1.conf" class="cbi-input-textarea" style="width: 100%;display:block;" data-update="change" rows="20" cols="80" readonly="readonly" > </textarea>
</div>
<script type="text/javascript">//<![CDATA[
const PARTEXP_RUN_URL = '<%=luci.dispatcher.build_url("admin", "system", "partexp","partexprun")%>';
const PARTEXP_CHECK = '<%=luci.dispatcher.build_url("admin", "system", "partexp","check")%>';
var checkbtn = document.getElementById('apply_run_button');
var islogreverse = false;
function reverselog(){
var lv = document.getElementById('cbid.logview.1.conf');
lv.innerHTML=lv.innerHTML.split('\n').reverse().join('\n')
if (islogreverse){
islogreverse=false;
}else{
islogreverse=true;
}
return
}
function apply_run(btn){
var sid='global'
var opt={
base:"cbid.partexp."+sid,
get:function(opt){
var id=this.base+'.'+opt;
var obj=document.getElementsByName(id)[0] || document.getElementsByClassName(id)[0] || document.getElementById(id)
if (obj){
return obj;
}else{
return null;
}
},
getlist:function(opt){
var id=this.base+'.'+opt;
var objs=document.getElementsByName(id) || document.getElementsByClassName(id);
var ret=[];
if (objs){
for (var i=0;i < objs.length;i++){
ret[i]=objs[i].value;
}
}else{
alert("<%:Fatal on get option,please help in debug%>:"+opt);
}
return ret;
},
query:function(param,src,tval="1",fval="0"){
var ret="&"+param+"=";
var obj=this.get(src);
if (obj){
if (obj.type=="checkbox"){
return ret+(obj.checked==true ? tval:fval);
}else{
return ret+encodeURIComponent(obj.value);
}
}
return ''
}
}
btn.value='<%:Waiting,(executing)...%>';
btn.disabled=true;
var targetf=opt.get("target_function").value;
if (opt.get("target_disk"))
var targetd=opt.get("target_disk").value;
else
var targetd="/";
if (opt.get("format_type"))
var eformat=opt.get("format_type").value;
else
var eformat = "0";
if (opt.get("keep_config"))
var kconfig = opt.get("keep_config").checked ? 1 : 0;
else
var kconfig = 0;
console.log(eformat);
XHR.get('<%=url([[admin]], [[system]], [[partexp]], [[partexprun]])%>',{
targetf: targetf,
kconfig: kconfig,
targetd: targetd,
eformat: eformat
},function(x){});
poll_check();
return
}
function poll_check(){
var tag = document.getElementById('logview');
tag.style.display="block"
XHR.poll(3, '<%=url([[admin]], [[system]], [[partexp]], [[check]])%>', null,
function(x, data) {
var lv = document.getElementById('cbid.logview.1.conf');
if (x.responseText && lv) {
if (x.responseText=="\u0000"){
for(j = 0,len=this.XHR._q.length; j < len; j++) {
if (this.XHR._q[j].url == '<%=url([[admin]], [[system]], [[partexp]], [[check]])%>'){
this.XHR._q.splice(j,1);
checkbtn.disabled = false;
checkbtn.value = '<%:Click to execute%>';
break;
}
}
return
}
if (islogreverse){
lv.innerHTML = x.responseText.split('\n').reverse().join('\n')+lv.innerHTML;
}else{
lv.innerHTML += x.responseText;
}
}
}
);}
//]]>
</script>
<%+cbi/valuefooter%>

View File

@@ -1,16 +0,0 @@
<%+cbi/valueheader%>
<textarea id="logview.list" class="cbi-input-textarea" style="width: 100%" rows="15" readonly="readonly"></textarea>
<script type="text/javascript">
const LOG_URL = '<%=luci.dispatcher.build_url("admin", "system", "partexp","realtime_log")%>';
XHR.poll(1, LOG_URL, null, (x, d) => {
let logview = document.getElementById("logview.list");
if (!d.running) {
XHR.halt();
}
logview.value = d.log;
logview.scrollTop = logview.scrollHeight;
});
</script>
<%+cbi/valuefooter%>

View File

@@ -1,54 +0,0 @@
<%+cbi/valueheader%>
<input type="button" class="btn cbi-button cbi-button-apply" id="apply_update_button" value="<%:Clear Log%>" onclick="apply_del_log()"/>
<input type="checkbox" name="NAME" value="reverse" onclick="reverselog()" style="vertical-align:middle;height:auto;"><%:Reverse%></input>
<textarea id="cbid.logview.1.conf" class="cbi-input-textarea" style="width: 100%;display:inline" data-update="change" rows="32" cols="60" readonly="readonly" > </textarea>
<script type="text/javascript">//<![CDATA[
var islogreverse=false;
function createAndDownloadFile(fileName,content){
var aTag=document.createElement('a');
var blob=new Blob([content]);
aTag.download=fileName;
aTag.href=URL.createObjectURL(blob);
aTag.click();
URL.revokeObjectURL(blob);
}
function apply_del_log(){
XHR.get('<%=url([[admin]],[[system]],[[partexp]],[[dellog]])%>',null,function(x,data){
var lv=document.getElementById('cbid.logview.1.conf');
lv.innerHTML="";
}
);
return
}
function reverselog(){
var lv=document.getElementById('cbid.logview.1.conf');
lv.innerHTML=lv.innerHTML.split('\n').reverse().join('\n')
if (islogreverse){
islogreverse=false;
}else{
islogreverse=true;
}
return
}
XHR.poll(3,'<%=url([[admin]],[[system]],[[partexp]],[[getlog]])%>',null,
function(x,data){
var lv=document.getElementById('cbid.logview.1.conf');
lv.innerHTML=""
if (x.responseText && lv){
if (islogreverse){
lv.innerHTML=x.responseText.split('\n').reverse().join('\n')+lv.innerHTML;
}else{
lv.innerHTML+=x.responseText;
}
}
}
)
//]]>
</script>
<%+cbi/valuefooter%>

View File

@@ -0,0 +1,199 @@
# Translation file for partexp.js
# Copyright (C) 2022-2025 Sirpdboy <herboy2008@gmail.com>
# Licensed to the public under the Apache License 2.0
#
msgid ""
msgstr ""
"Project-Id-Version: partexp\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: \n"
"Language-Team: Chinese (Simplified)\n"
"Language: zh_Hans\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Generated from partexp.js\n"
#: JavaScript UI strings
msgid "One click partition expansion mounting tool"
msgstr ""
msgid "Automatically format and mount the target device partition. If there are multiple partitions, it is recommended to manually delete all partitions before using this tool."
msgstr ""
msgid "For specific usage, see:"
msgstr ""
msgid "Select function"
msgstr ""
msgid "Used to extend to the root directory of EXT4 firmware(Ext4 /)"
msgstr ""
msgid "Expand application space overlay (/overlay)"
msgstr ""
msgid "Used as Docker data disk (/opt)"
msgstr ""
msgid "Normal mount and use by device name(/mnt/x1)"
msgstr ""
msgid "Select the function to be performed"
msgstr ""
msgid "Destination hard disk"
msgstr ""
msgid "Loading devices..."
msgstr ""
msgid "Select the hard disk device to operate"
msgstr ""
msgid "Keep configuration"
msgstr ""
msgid "Tick means to retain the settings"
msgstr ""
msgid "Format system type"
msgstr ""
msgid "No formatting required"
msgstr ""
msgid "Linux system partition(EXT4)"
msgstr ""
msgid "Large capacity storage devices(Btrfs)"
msgstr ""
msgid "Windows system partition(NTFS)"
msgstr ""
msgid "Perform operation"
msgstr ""
msgid "Click to execute"
msgstr ""
msgid "Starting operation..."
msgstr ""
msgid "Operation Log"
msgstr ""
msgid "Are you sure you want to execute partition expansion?"
msgstr ""
msgid "Function:"
msgstr ""
msgid "Disk:"
msgstr ""
msgid "Keep config:"
msgstr ""
msgid "Format type:"
msgstr ""
msgid "This operation may take several minutes."
msgstr ""
msgid "Executing..."
msgstr ""
msgid "Operation started successfully"
msgstr ""
msgid "Operation failed"
msgstr ""
msgid "Failed to start operation:"
msgstr ""
msgid "Unknown error"
msgstr ""
msgid "Operation in progress..."
msgstr ""
msgid "Operation in progress..."
msgstr ""
msgid "Operation in progress..."
msgstr ""
msgid "Extend to root directory"
msgstr ""
msgid "Expand overlay"
msgstr ""
msgid "Docker data disk"
msgstr ""
msgid "Normal mount"
msgstr ""
msgid "No formatting"
msgstr ""
msgid "Operation completed"
msgstr ""
msgid "Getting device information"
msgstr ""
msgid "Checking partition format"
msgstr ""
msgid "Checking target device"
msgstr ""
msgid "Locating target partition"
msgstr ""
msgid "Checking free space"
msgstr ""
msgid "Starting operation"
msgstr ""
msgid "Initializing..."
msgstr ""
msgid "Starting"
msgstr ""
#: Device status messages
msgid "Loading device list..."
msgstr ""
msgid "No devices found"
msgstr ""
msgid "Failed to load devices"
msgstr ""
msgid "Please select a target disk"
msgstr ""
msgid "Settings saved"
msgstr ""
msgid "Failed to save settings"
msgstr ""
msgid "Loading saved configuration..."
msgstr ""
msgid "Configuration loaded"
msgstr ""
msgid "Failed to load configuration"
msgstr ""

View File

@@ -1,96 +0,0 @@
msgid ""
msgstr ""
"Copyright (C) 2022-2025 sirpdboy herboy2008@gmail.com https://github.com/sirpdboy/luci-app-partexp"
"This is free software, licensed under the GNU General Public License v3."
msgid "Partition Expansion"
msgstr "分区扩容"
msgid "One click partition expansion mounting tool"
msgstr "一键分区扩容挂载工具"
msgid "Automatically format and mount the target device partition. If there are multiple partitions, it is recommended to manually delete all partitions before using this tool.<br/>For specific usage, see:"
msgstr "自动对目标设备分区格式化挂载,如果有多分区建议手动删除所有分区再使用本工具.<br/>使用说明见:"
msgid "Waiting,(executing)..."
msgstr "稍等,努力执行中"
msgid "Expand application space overlay (/overlay)"
msgstr "用于overlay软件空间 (/overlay)"
msgid "Used to extend to the root directory of EXT4 firmware(Ext4 /)"
msgstr "用于扩展为EXT4固件根目录(Ext4 /)"
msgid "Used as Docker data disk (/opt)"
msgstr "用作Docker数据盘 (/opt)"
msgid "Normal mount and use by device name(/mnt/x1)"
msgstr "按设备名普通挂载使用(/mnt/x1)"
msgid "Soft chain partition expansion(/overlay)"
msgstr "分区软链扩容(/overlay)"
msgid "Destination hard disk"
msgstr "目标硬盘"
msgid "Keep configuration"
msgstr "保留配置"
msgid "Format system type"
msgstr "格式化系统类型"
msgid "No formatting required"
msgstr "不需要格式化"
msgid "Linux system partition(EXT4)"
msgstr "Linux系统分区(EXT4)"
msgid "Large capacity storage devices(Btrfs)"
msgstr "大容量存储设备(Btrfs)"
msgid "Windows system partition(NTFS)"
msgstr "Windows系统分区(NTFS)"
msgid "Select the hard disk device to operate"
msgstr "选择需要操作的硬盘设备"
msgid "Select function"
msgstr "选择功能"
msgid "Select the function to be performed"
msgstr "选择要执行的功能"
msgid "Click to execute"
msgstr "点击执行"
msgid "Perform operation"
msgstr "执行操作"
msgid "To make the operation effective, the device will restart. Are you sure to execute?"
msgstr "警告:操作一旦确定无法取消,设备将会重启,是否确定执行?"
msgid "Operation in progress, please wait..."
msgstr "操作执行中,请稍候..."
msgid "After operation, restart the machine, please wait..."
msgstr "操作完毕,机器重启,请稍候..."
msgid "Please delete the partition or share and try again"
msgstr "错误,请检查是否有足够空间或是共享使用中。"
msgid "Restart the device to take effect. Confirm whether to continue?"
msgstr "重启设备操作才生效,确定是否继续执行?"
msgid "Operation execution complete"
msgstr "操作执行完毕"
msgid "Ticking indicates formatting"
msgstr "打勾选择表示格式化"
msgid "Tick means to retain the settings"
msgstr "打勾选择表示保留设置"
msgid "reverse"
msgstr "逆序"

View File

@@ -1,46 +1,72 @@
# Translation file for partexp.js
# Copyright (C) 2022-2025 Sirpdboy <herboy2008@gmail.com>
# Licensed to the public under the Apache License 2.0
#
msgid ""
msgstr ""
"Copyright (C) 2022-2025 sirpdboy herboy2008@gmail.com https://github.com/sirpdboy/luci-app-partexp"
"This is free software, licensed under the GNU General Public License v3."
"Project-Id-Version: partexp\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: \n"
"Language-Team: Chinese (Simplified)\n"
"Language: zh_Hans\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Generated from partexp.js\n"
msgid "Partition Expansion"
msgstr "分区扩容"
#: JavaScript UI strings
msgid "One click partition expansion mounting tool"
msgstr "一键分区扩容挂载工具"
msgid "Automatically format and mount the target device partition. If there are multiple partitions, it is recommended to manually delete all partitions before using this tool.<br/>For specific usage, see:"
msgstr "自动对目标设备分区格式化挂载,如果有多分区建议手动删除所有分区再使用本工具.<br/>使用说明见:"
msgid "Partition Expansion"
msgstr "分区扩容"
msgid "Waiting,(executing)..."
msgstr "稍等,努力执行中"
msgid "Automatically format and mount the target device partition. If there are multiple partitions, it is recommended to manually delete all partitions before using this tool."
msgstr "自动格式化并挂载目标设备分区。如果有多个分区,建议在使用此工具前手动删除所有分区。"
msgid "Expand application space overlay (/overlay)"
msgstr "用于overlay软件空间 (/overlay)"
msgid "For specific usage, see:"
msgstr "具体用法请参见:"
msgid "Select function"
msgstr "选择功能"
msgid "Used to extend to the root directory of EXT4 firmware(Ext4 /)"
msgstr "用于扩展EXT4固件根目录(Ext4 /)"
msgstr "用于扩展EXT4固件根目录(Ext4 /)"
msgid "Expand application space overlay (/overlay)"
msgstr "扩展应用空间overlay (/overlay)"
msgid "Used as Docker data disk (/opt)"
msgstr "用作Docker数据盘 (/opt)"
msgid "Normal mount and use by device name(/mnt/x1)"
msgstr "按设备名普通挂载使用(/mnt/x1)"
msgstr "普通挂载并按设备名称使用(/mnt/x1)"
msgid "Soft chain partition expansion(/overlay)"
msgstr "分区软链扩容(/overlay)"
msgid "Select the function to be performed"
msgstr "选择要执行的功能"
msgid "Destination hard disk"
msgstr "目标硬盘"
msgid "Loading devices..."
msgstr "正在加载设备..."
msgid "Select the hard disk device to operate"
msgstr "选择要操作的硬盘设备"
msgid "Keep configuration"
msgstr "保留配置"
msgid "Tick means to retain the settings"
msgstr "勾选表示保留设置"
msgid "Format system type"
msgstr "格式化系统类型"
msgid "No formatting required"
msgstr "不需要格式化"
msgstr "无需格式化"
msgid "Linux system partition(EXT4)"
msgstr "Linux系统分区(EXT4)"
@@ -51,46 +77,126 @@ msgstr "大容量存储设备(Btrfs)"
msgid "Windows system partition(NTFS)"
msgstr "Windows系统分区(NTFS)"
msgid "Select the hard disk device to operate"
msgstr "选择需要操作的硬盘设备"
msgid "Select function"
msgstr "选择功能"
msgid "Select the function to be performed"
msgstr "选择要执行的功能"
msgid "Perform operation"
msgstr "执行操作"
msgid "Click to execute"
msgstr "点击执行"
msgid "Perform operation"
msgstr "执行操作"
msgid "Starting operation..."
msgstr "正在启动操作..."
msgid "To make the operation effective, the device will restart. Are you sure to execute?"
msgstr "警告:操作一旦确定无法取消,设备将会重启,是否确定执行?"
msgid "Operation Log"
msgstr "操作日志"
msgid "Operation in progress, please wait..."
msgstr "操作执行中,请稍候..."
msgid "Are you sure you want to execute partition expansion?"
msgstr "确定要执行分区扩容吗?"
msgid "After operation, restart the machine, please wait..."
msgstr "操作完毕,机器重启,请稍候..."
msgid "Function:"
msgstr "功能:"
msgid "Please delete the partition or share and try again"
msgstr "错误,请检查是否有足够空间或是共享使用中。"
msgid "Disk:"
msgstr "磁盘:"
msgid "Restart the device to take effect. Confirm whether to continue?"
msgstr "重启设备操作才生效,确定是否继续执行?"
msgid "Keep config:"
msgstr "保留配置:"
msgid "Operation execution complete"
msgstr "操作执行完毕"
msgid "Format type:"
msgstr "格式化类型:"
msgid "Ticking indicates formatting"
msgstr "打勾选择表示格式化"
msgid "This operation may take several minutes."
msgstr "此操作可能需要几分钟时间。"
msgid "Tick means to retain the settings"
msgstr "打勾选择表示保留设置"
msgid "Executing..."
msgstr "正在执行..."
msgid "reverse"
msgstr "逆序"
msgid "Operation started successfully"
msgstr "操作启动成功"
msgid "Operation failed"
msgstr "操作失败"
msgid "Failed to start operation:"
msgstr "启动操作失败:"
msgid "Unknown error"
msgstr "未知错误"
msgid "Operation in progress..."
msgstr "操作正在进行中..."
msgid "Operation in progress..."
msgstr "操作正在进行中..."
msgid "Operation in progress..."
msgstr "操作正在进行中..."
msgid "Extend to root directory"
msgstr "扩展到根目录"
msgid "Expand overlay"
msgstr "扩展overlay"
msgid "Docker data disk"
msgstr "Docker数据盘"
msgid "Normal mount"
msgstr "普通挂载"
msgid "No formatting"
msgstr "不格式化"
msgid "Operation completed"
msgstr "操作完成"
msgid "Getting device information"
msgstr "正在获取设备信息"
msgid "Checking partition format"
msgstr "正在检查分区格式"
msgid "Checking target device"
msgstr "正在检查目标设备"
msgid "Locating target partition"
msgstr "正在定位目标分区"
msgid "Checking free space"
msgstr "正在检查可用空间"
msgid "Starting operation"
msgstr "正在开始操作"
msgid "Initializing..."
msgstr "正在初始化..."
msgid "Starting"
msgstr "开始"
#: Device status messages
msgid "Loading device list..."
msgstr "正在加载设备列表..."
msgid "No devices found"
msgstr "未找到设备"
msgid "Failed to load devices"
msgstr "加载设备失败"
msgid "Please select a target disk"
msgstr "请选择目标磁盘"
msgid "Settings saved"
msgstr "设置已保存"
msgid "Failed to save settings"
msgstr "保存设置失败"
msgid "Loading saved configuration..."
msgstr "正在加载保存的配置..."
msgid "Configuration loaded"
msgstr "配置已加载"
msgid "Failed to load configuration"
msgstr "加载配置失败"

View File

@@ -1,5 +1,5 @@
config global 'global'
option target_function '/opt'
option target_disk ''
option keep_config '0'
option format_type '0'
config global 'global'
option target_function '/opt'
option target_disk ''
option keep_config '0'
option format_type '0'

View File

@@ -1,751 +0,0 @@
#!/bin/sh /etc/rc.common
#
# Copyright (C) 2021-2025 sirpdboy <herboy2008@gmail.com> https://github.com/sirpdboy/luci-app-partexp
# This is free software, licensed under the Apache License, Version 2.0 .
#
START=99
USE_PROCD=1
EXTRA_COMMANDS="autopart"
CONFIG="taskplan"
LOCK=/var/lock/$CONFIG.lock
LOGD=/var/$CONFIG
LOGDIR=/etc/$CONFIG
LOG=$LOGDIR/bk$CONFIG.log
[ -d "$LOGDIR" ] || mkdir -p $LOGDIR
[ -d "$LOGD" ] || mkdir -p $LOGD
limit_log() {
local logf=$1
[ ! -f "$logf" ] && return
local sc=100
[ -n "$2" ] && sc=$2
local count=$(grep -c "" $logf)
if [ $count -gt $sc ];then
let count=count-$sc
sed -i "1,$count d" $logf
fi
}
init_env() {
[ ! -f "$LOG" ] && echo " " > $LOG
}
gen_log()(
log "--自动分区扩展挂载开始执行-- " | tee -a $LOG
)
log(){
echo -e " $(date +'%Y-%m-%d %H:%M:%S') $*" | tee -a $LOG
}
# 检查硬盘是否已挂载
is_disk_mounted() {
DISK=$1
if mount | grep -q "$DISK "; then
return 0 # 已挂载
else
return 1 # 未挂载
fi
}
mount_device() {
local DEVICE=$1
local MOUNT_POINT=$2
local TYPE=$3
# 检查设备是否存在
if [ ! -e "$DEVICE" ]; then
log "设备 $DEVICE 不存在"
return 1
fi
# 检查挂载点是否存在
if [ ! -d "$MOUNT_POINT" ]; then
log "挂载点 $MOUNT_POINT 不存在,正在创建..."
mkdir -p "$MOUNT_POINT"
if [ $? -ne 0 ]; then
log "无法创建挂载点 $MOUNT_POINT"
return 1
fi
fi
# 检查设备是否已挂载
if mount | grep -q "$DEVICE"; then
log "设备 $DEVICE 已挂载到其他位置"
return 1
fi
if mount | grep -q "$MOUNT_POINT"; then
log "挂载点 $MOUNT_POINT 已被其他设备占用"
return 1
fi
# 挂载设备
log "正在挂载 $DEVICE 到 $MOUNT_POINT..."
# mount "$DEVICE" "$MOUNT_POINT"
mount $TYPE "$DEVICE" "$MOUNT_POINT" >/dev/null 2> /dev/null
# 检查挂载是否成功
if [ $? -eq 0 ]; then
log "挂载成功: $DEVICE -> $MOUNT_POINT"
return 0
else
log "挂载失败: $DEVICE -> $MOUNT_POINT"
return 1
fi
}
# 取消硬盘挂载
umount_disk() {
DISK=$1
MOUNT=''
eval $(block info "$DISK" | grep -o -e "MOUNT=\S*")
if [ "$MOUNT" ]; then
umount $DISK 2>/dev/null
if [ $? -eq 0 ]; then
log "取消挂载成功:$DISK"
else
log "取消挂载失败:$DISK"
fi
else
log "设备/dev/$DISK未挂载"
fi
}
# 从 block info 中提取指定字段的值
get_block() {
local DISK=$1
local TYPE=$2
local value
value=`mount | grep $DISK |awk -F $TYPE '{print $2}' |awk '{print $1}' | head -1`
# value=$(block info "/dev/$DISK" | grep -o -e "$TYPE=\S*" | cut -d\" -f2)
echo $value
}
# 检查是否有共享挂载(如 Samba 或 NFS
check_shared_mount() {
DISK=$1
if [ -f /etc/config/samba ]; then
SHARED=$(grep -q "/dev/$DISK" /etc/config/samba)
if [ $? -eq 0 ]; then
log "检测到 Samba 共享挂载: /dev/$DISK"
return 0
fi
fi
# 检查是否有 NFS 共享
if [ -f /etc/exports ]; then
SHARED=$(grep -q "/dev/$DISK" /etc/exports)
if [ $? -eq 0 ]; then
log "检测到 NFS 共享挂载: /dev/$DISK"
return 0
fi
fi
return 1
}
usamba(){
s=$1
[ -e "/etc/config/$s" ] && {
msum=$(grep -c "config sambashare" /etc/config/$s)
for i in $(seq 0 $((msum)));do
pdev=`uci -q get $s.@sambashare[$i].path `
[ "$pdev" = "$2" ] && {
uci delete $s.@sambashare[$i]
uci commit $s
log "分区/dev/$b被挂载$MOUNT共享使用删除$s共享成功"
sleep 5
}
done
}
# 取消 Samba 共享
if [ -f /etc/config/$s ]; then
sed -i "/\/dev\/$b/d" /etc/config/$s
/etc/init.d/$s restart
fi
# 取消 NFS 共享
if [ -f /etc/exports ]; then
sed -i "/\/dev\/$b/d" /etc/exports
/etc/init.d/nfs restart
fi
}
is_disk_partitioned() {
PARTITION_COUNT=$(fdisk -l /dev/$1 2>/dev/null | grep -E "^/dev/$2" | wc -l)
if [[ "$PARTITION_COUNT" -gt 0 ]]; then
echo 1
else
echo 0
fi
}
partednew(){
DISK=$1
parted -s /dev/$DISK mklabel gpt
parted -s /dev/$DISK mkpart primary ext4 1MiB -1
}
fdisknew(){
echo -e "n\np\n\n\n\nw" | fdisk /dev/$1 >/dev/null 2> /dev/null
}
fdisksave(){
echo -e "n\w" | fdisk /dev/$1 >/dev/null 2> /dev/null
}
# 格式化磁盘函数 DISK=/dev/sda1 ;TYPE=btrfs
format_disk() {
local DISK=$1
local TYPE=$2
[[ $TYPE == '0' || $TYPE == '' ]] && TYPE="ext4"
log "正在格式化 $DISK "
mkfs.$TYPE -F "$DISK" >/dev/null 2>/dev/null
if [ $? -eq 0 ]; then
log "格式化 $TYPE 成功 $DISK"
return 0
else
log "格式化 $TYPE 失败 $DISK"
return 1
fi
}
fdiskB(){
a=$1
b=$1$2
log "开始检测目标$a信息"
log "检测/dev/$a是否需要分区和格式化$format_type"
block detect > /etc/config/fstab
uci -q set fstab.@global[0].anon_mount='0'
uci -q set fstab.@global[0].auto_mount='0'
uci commit fstab
if [ $target_function = '/opt' ] ;then
/etc/init.d/dockerd stop >/dev/null 2> /dev/null
amount=`mount |grep /opt | awk '{print $1}'`
if [ -n "$amount" ] ;then
umount $amount >/dev/null 2> /dev/null
log "取消/opt之前的挂载$amount成功"
fi
for OPT in $(mount |grep /opt | awk '{print $3}');do
umount $OPT >/dev/null 2> /dev/null
log "取消/opt之前的挂载$OPT成功"
done
fi
[ -d "/mnt/$b" ] || mkdir -p /mnt/$b
if is_disk_mounted "/dev/$b"; then
log "设备 /dev/$b 已挂载,尝试取消挂载..."
if check_shared_mount $b; then
usamba samba4 $MOUNT
usamba samba $MOUNT
sleep 5
fi
umount_disk "/dev/$b"
[ $? -ne 0 ] || umount_disk "/mnt/$b"
else
log "设备/dev/$b未挂载"
isfdisk=0
isP=$(is_disk_partitioned $a $b)
if [ "$isP" = '0' ] ;then
fdisksave $a
fdisknew $a
sleep 2
isfdisk=1
fi
isP=$(is_disk_partitioned $a $b)
if [[ "$isP" = '1' && "$isfdisk" = 1 ]] ;then
log "分区$b建立成功"
elif [[ "$isP" = '1' && "$isfdisk" = 0 ]] ;then
log "检测目标分区$b已存在."
else
log "分区$b建立失败请检查$b硬盘空间"
expquit 1
fi
sleep 1
fi
if is_disk_mounted "/dev/$b"; then
umount /dev/$b >/dev/null 2> /dev/null
[ $? -ne 0 ] && block umount /dev/$b >/dev/null 2> /dev/null
fi
if [[ "$target_function" = "/" || "$target_function" = "/overlay" ]] ; then
format_disk "/dev/$b" $format_type
elif [[ "$format_type" != "0" || "$isfdisk" = "1" ]] ; then
format_disk "/dev/$b" $format_type
else
log "设备/dev/$b如果未格式化,可能无法正常使用."
fi
TYPE='';eval $(blkid "/dev/$b" | grep -o -e "TYPE=\S*")
log "检测设备/dev/$b分区$TYPE格式"
if [ "$TYPE" = "ntfs" ];then
if [ `which ntfs-3g ` ] ;then
if is_disk_mounted "/mnt/$b" ;then
mount_device /dev/$b /mnt/$b "-t ntfs-3g"
fi
else
if is_disk_mounted "/mnt/$b" ;then
mount_device /dev/$b /mnt/$b "-t ntfs3"
fi
fi
else
mount /dev/$b /mnt/$b >/dev/null 2> /dev/null
fi
UUID='';eval $(block info /dev/$b | grep -o -e "UUID=\S*")
if [ ! "$UUID" ] ; then
log "获取/dev/$b设备UUID信息失败"
expquit 1
else
log "获取/dev/$b设备UUID信息:$UUID成功"
fi
case "$target_function" in
"/overlay")
if [ "$keep_config" = "1" ] ; then
# cp -a -f /overlay/* /mnt/$b/ || cp -a -f /rom/overlay/* /mnt/$b/
tar -C /overlay -cvf - . | tar -C /mnt/$b/ -xf - || tar -C /rom/overlay -cvf - . | tar -C /mnt/$b/ -xf -
umount /dev/$b >/dev/null 2> /dev/null
[ $? -ne 0 ] && umount /mnt/$b >/dev/null 2> /dev/null
[ $? -ne 0 ] && block umount /dev/$b >/dev/null 2> /dev/null
block detect > /etc/config/fstab
OVERLAY=`uci -q get fstab.@mount[0].target `
if [[ "$OVERLAY" = "/overlay" || "$OVERLAY" = "/dev/loop0" ]] ;then
uci -q set fstab.@mount[0].uuid="${UUID}"
uci -q set fstab.@mount[0].target='/overlay'
uci -q set fstab.@mount[0].enabled='0'
fi
msum=$(grep -c "'mount'" /etc/config/fstab)
for i in $(seq 0 $((msum-1)))
do
zuuid=`uci -q get fstab.@mount[$i].uuid `
[ $? -ne 0 ] && break
if [ "$zuuid" = "$UUID" ] ; then
uci -q set fstab.@mount[$i].target="/overlay"
uci -q set fstab.@mount[$i].enabled='1'
fi
done
uci set fstab.@global[0].delay_root="15"
uci commit fstab
log "保留数据overlay扩展/dev/$b成功"
eval $(block info /dev/$b | grep -o -e "MOUNT=\S*")
echo $MOUNT $a>> /etc/partexppath
sleep 3
log "设备重启才能生效"
expquit 2
else
umount /dev/$b >/dev/null 2> /dev/null
[ $? -ne 0 ] && umount /mnt/$b >/dev/null 2> /dev/null
[ $? -ne 0 ] && block umount /dev/$b >/dev/null 2> /dev/null
block detect > /etc/config/fstab
OVERLAY=`uci -q get fstab.@mount[0].target `
if [[ "$OVERLAY" = "/overlay" || "$OVERLAY" = "/dev/loop0" ]] ;then
uci -q set fstab.@mount[0].uuid="${UUID}"
uci -q set fstab.@mount[0].target='/overlay'
uci -q set fstab.@mount[0].enabled='0'
fi
msum=$(grep -c "'mount'" /etc/config/fstab)
for i in $(seq 0 $((msum-1)))
do
zuuid=`uci -q get fstab.@mount[$i].uuid `
[ $? -ne 0 ] && break
if [ "$zuuid" = "$UUID" ] ; then
uci -q set fstab.@mount[$i].target="/overlay"
uci -q set fstab.@mount[$i].enabled='1'
fi
done
uci set fstab.@global[0].delay_root="15"
uci commit fstab
log "不保留数据overlay扩展/dev/$b成功"
eval $(block info /dev/$b | grep -o -e "MOUNT=\S*")
echo $MOUNT $a>> /etc/partexppath
sleep 3
log "设备重启才能生效"
expquit 2
fi
;;
"/opt")
umount /dev/$b >/dev/null 2> /dev/null
[ $? -ne 0 ] && umount /mnt/$b >/dev/null 2> /dev/null
[ $? -ne 0 ] && block umount /dev/$b >/dev/null 2> /dev/null
block detect > /etc/config/fstab
mkdir -p $target_function
msum=$(grep -c "'mount'" /etc/config/fstab)
mount_device /dev/$b "$target_function"
for i in $(seq 0 $((msum-1)))
do
zuuid=`uci -q get fstab.@mount[$i].uuid `
[ $? -ne 0 ] && break
if [ "$zuuid" = "$UUID" ] ; then
uci -q set fstab.@mount[$i].target="$target_function"
uci -q set fstab.@mount[$i].enabled='1'
fi
done
uci commit fstab
# ln -sf /mnt/$b /overlay
if is_disk_mounted "/opt"; then
log "/dev/$b分区扩容和挂载到$target_function成功"
eval $(block info /dev/$b | grep -o -e "MOUNT=\S*")
echo $MOUNT $a>> /etc/partexppath
log "如果没生效,请重启设备"
expquit 2
else
log "/dev/$b分区扩容和挂载到$target_function失败"
fi
;;
"/")
ROOTBLK="$(readlink -f /sys/dev/block/"$(awk '$9="/dev/root"{print $3}' /proc/self/mountinfo)")"
[ -z "$ROOTBLK" ] && { log "错误:无法获取根分区块设备"; expquit 1; }
ROOTDISK="/dev/$(basename "${ROOTBLK%/}")"
FSTYPE=$(blkid -o value -s TYPE "$ROOTDISK" 2>/dev/null)
if [[ "$FSTYPE" != "squashfs" && -n "$FSTYPE" ]] ; then
if [ $target_function = '/' ] ;then
FREE_SPACE=$(check_free_space $(basename $DISK))
log "目标盘 $ROOT_PART $FSTYPE有剩余空间: $FREE_SPACE Gb"
if [[ "$FREE_SPACE" -gt 2 ]]; then
rootpt_resize
expquit 2
else
log "目标盘 $SYSTEM_DISK $FSTYPE没有足够的剩余空间"
expquit 1
fi
fi
if [ $target_function = '/overlay' ] ;then
FREE_SPACE=$(check_free_space $(basename $DISK))
log "目标盘 $ROOT_PART $FSTYPE有剩余空间: $FREE_SPACE Gb"
if [[ "$FREE_SPACE" -gt 2 ]]; then
rootfs_resize
eval $(block info /dev/$b | grep -o -e "MOUNT=\S*")
echo $MOUNT $a>> /etc/partexppath
expquit 2
else
log "目标盘 $SYSTEM_DISK $FSTYPE没有足够的剩余空间"
expquit 1
fi
fi
else
log "目标硬盘不支持/根分区扩展请换EXT4固件"
fi
sleep 3
expquit 2
;;
*)
umount /dev/$b >/dev/null 2> /dev/null
[ $? -ne 0 ] && umount /mnt/$b >/dev/null 2> /dev/null
[ $? -ne 0 ] && block umount /dev/$b >/dev/null 2> /dev/null
block detect > /etc/config/fstab
mkdir -p $target_function
msum=$(grep -c "'mount'" /etc/config/fstab)
mount_device /dev/$b /mnt/$b
for i in $(seq 0 $((msum-1)))
do
zuuid=`uci -q get fstab.@mount[$i].uuid `
[ $? -ne 0 ] && break
if [ "$zuuid" = "$UUID" ] ; then
uci -q set fstab.@mount[$i].target="/mnt/$b"
uci -q set fstab.@mount[$i].enabled='1'
fi
done
uci commit fstab
if is_disk_mounted /mnt/$b ; then
log "/dev/$b分区扩容和挂载到/mnt/$b成功"
eval $(block info /dev/$b | grep -o -e "MOUNT=\S*")
echo $MOUNT $a>> /etc/partexppath
log "如果没生效,请重启设备"
expquit 2
else
log "/dev/$b分区扩容和挂载到/mnt/$b失败"
fi
;;
esac
}
get_system_disk() {
SYSTEM_DISK=$(df -h | grep boot | awk '{print $1}' | head -1 | sed -E 's/(p?[0-9]+)$//')
[ -z ${SYSTEM_DISK} ] && SYSTEM_DISK=$(mount | grep 'on /overlay' | awk '{print $1}' | sed -E 's/(p?[0-9]+)$//' |head -1)
echo "$SYSTEM_DISK"
ROOT_DISK="/dev/$(basename "${ROOTBLK%/*}")"
echo "$ROOT_DISK"
}
get_all_disks() {
DISKS=`find /dev -regex '.*/\(sd[a-z]\|mmcblk[0-9]\+\|sata[a-z]\|nvme[0-9]\+n[0-9]\+\|vd[a-z]\)$'`
echo "$DISKS"
}
check_part_space() {
DISK=$1
info=$(lsblk -no SIZE,FSTYPE,MOUNTPOINT "$DISK" | awk '{print $1}')
if [ -z "$info" ]; then
echo "物理大小: 未知(可能是未格式化的裸分区)"
else
echo $info |awk -F '.' '{print $1}' | sed 's/[A-Za-z]//g'
fi
}
check_free_space() {
DISK=$1
PARTED_OUTPUT=$(parted -s /dev/$DISK unit GB print free 2>/dev/null)
FREE_SPACE=$(echo "$PARTED_OUTPUT" | grep "Free Space" | awk '{print $3}' )
echo $FREE_SPACE |awk -F '.' '{print $1}' | sed 's/[A-Za-z]//g'
}
show_partition_info() {
local partition="$1"
if [ ! -e "$partition" ]; then
echo "错误:分区 $partition 不存在!"
return 1
fi
echo -e "\n=== 分区信息 [$partition] ==="
local lsblk_info=$(lsblk -no SIZE,FSTYPE,MOUNTPOINT "$partition" 2>/dev/null)
if [ -z "$lsblk_info" ]; then
echo "物理大小: 未知(可能是未格式化的裸分区)"
else
local size=$(echo "$lsblk_info" | awk '{print $1}')
local fstype=$(echo "$lsblk_info" | awk '{print $2}')
local mountpoint=$(echo "$lsblk_info" | awk '{print $3}')
echo "物理大小: $size"
echo "文件系统: ${fstype:-未知}"
echo "挂载点: ${mountpoint:-未挂载}"
fi
if df "$partition" &>/dev/null; then
local df_info=$(df -h "$partition" | awk 'NR=2 {print $2,$3,$4,$5}')
echo -e "\n[已挂载] 空间使用情况:"
echo "总容量: $(echo "$df_info" | awk '{print $1}')"
echo "已用: $(echo "$df_info" | awk '{print $2}')"
echo "剩余: $(echo "$df_info" | awk '{print $3}')"
echo "使用率: $(echo "$df_info" | awk '{print $4}')"
else
echo -e "\n[未挂载] 无法查询使用情况(需先挂载)"
fi
local disk="${partition%[0-9]*}"
local part_num="${partition##*[!0-9]}"
echo -e "\n分区表信息:"
parted -s "$disk" unit MiB print | grep -w "^ $part_num" | awk '{print "起始: " $2 " MiB | 结束: " $3 " MiB | 类型: " $6}'
}
get_next_partition_number() {
DISK=$1
PARTITIONS=$(fdisk -l /dev/$DISK 2>/dev/null | grep -v boot | grep -E "^/dev/$DISK" | awk '{print $1}' | sed 's/\/dev\/[a-z]*//g' | awk -F '[^0-9]+' '{print $NF}')
MAX_PARTITION=$(echo "$PARTITIONS" | sort -n | tail -n 1)
NEXT_PARTITION=$(awk -v n="$MAX_PARTITION" 'BEGIN { print n + 1 }')
#NEXT_PARTITION=$((MAX_PARTITION + 1))
echo "$NEXT_PARTITION"
}
get_last_partition_number() {
DISK=$1
PARTITIONS=$(fdisk -l /dev/$DISK 2>/dev/null | grep -v boot | grep -E "^/dev/$DISK" | awk '{print $1}' | sed 's/\/dev\/[a-z]*//g' | awk -F '[^0-9]+' '{print $NF}')
MAX_PARTITION=$(echo "$PARTITIONS" | sort -n | tail -n 1)
echo "$MAX_PARTITION"
}
get_partition_number() {
DISK=$1
PARTITIONS=$(fdisk -l /dev/$DISK 2>/dev/null | grep -v boot | grep -E "^/dev/$DISK" | awk '{print $1}' | sed 's/\/dev\/[a-z]*//g' | wc -l)
echo "$PARTITIONS"
}
rootpt_resize()
{
if [ ! -e /etc/rootpt-resize ] ;then
log "--->请稍侯,系统根分区扩展中<---"
ROOTBLK="$(readlink -f /sys/dev/block/"$(awk -e '$9="/dev/root"{print $3}' /proc/self/mountinfo)")"
ROOTDISK="/dev/$(basename "${ROOTBLK%/}")"
ROOTPART="${ROOTBLK##*[^0-9]}"
partplace=$(fdisk -l 2>/dev/null | grep "$ROOTDISK" | awk '{print $5}' )
log "--->根分区$ROOTDISK$partplace <---"
sleep 3
parted -f -s "${ROOTDISK}" resizepart "${ROOTPART}" 100%
mount_root done
touch /etc/rootpt-resize
sleep 3
log "--->系统根分区扩展成功!<---"
partplace=$(fdisk -l 2>/dev/null | grep "$ROOTDISK" | awk '{print $5}' )
log "--->根分区$ROOTDISK扩展后容量$partplace <---"
log "--->如果没生效,请重启设备<---"
expquit 2
else
log "已经扩展过或者挂载分区过请删除分区或者重置重新操作或者联系作者sirpdboy"
expquit 1
fi
}
rootfs_resize()
{
if [ ! -e /etc/rootfs-resize ] && [ -e /etc/rootpt-resize ] ;then
log "--->请稍侯,系统根分区扩展中<---"
ROOTBLK="$(readlink -f /sys/dev/block/"$(awk -e '$9="/dev/root"{print $3}' /proc/self/mountinfo)")"
ROOTPART="${ROOTBLK##*[^0-9]}"
ROOTDISK="/dev/$(basename "${ROOTBLK%/}")"
partplace=$(fdisk -l 2>/dev/null | grep "$ROOTDISK" | awk '{print $5}' )
log "--->根分区$ROOTDISK$partplace <---"
sleep 3
parted -f -s "${ROOTDISK}" resizepart "${ROOTPART}" 100%
mount_root done
touch /etc/rootpt-resize
sleep 3
log "--->系统根分区扩展成功!<---"
partplace=$(fdisk -l 2>/dev/null | grep "$ROOTDISK" | awk '{print $5}' )
log "--->根分区$ROOTDISK扩展后容量$partplace <---"
log "--->请稍侯系统overlay扩展中<---"
df -h /overlay | awk 'NR=2 {printf " overlay扩展前: 总容量: %s 已用: %s 剩余: %s 使用率: %s", $2, $3, $4, $5}'
LOOPDEV="$(awk -e '$5="/overlay"{print $9}' /proc/self/mountinfo)"
if [ -z "${LOOPDEV}" ] ; then
LOOP_DEV="$(losetup -f)"
losetup "${LOOPDEV}" "${ROOTDEV}"
fi
# eval $(blkid "$LOOPDEV" | grep -o -e "TYPE=\S*")
FSTYPE=$(blkid -o value -s TYPE "$LOOPDEV" 2>/dev/null)
umount -l /overlay
mount -t tmpfs -o size=128M tmpfs /overlay
losetup -d /dev/loop0
losetup -fP ${ROOTDISK}
case "$FSTYPE" in
f2fs)
umount /overlay || { log "错误:无法卸载 /overlay"; expquit 1; }
fsck.f2fs -f "$LOOPDEV"
resize.f2fs -f "$LOOPDEV" || { log "错误f2fs 调整大小失败"; expquit 1; }
;;
ext4)
resize2fs -f "$LOOPDEV" || { log "错误ext4 调整大小失败"; expquit 1; }
;;
*)
log "--->分区格式 $FSTYPE 不识别overlay 扩展失败!<---"
expquit 1
;;
esac
mount_root done
touch /etc/rootfs-resize
sleep 3
log "--->系统overlay扩展成功<---"
df -h /overlay | awk 'NR=2 {printf " overlay扩展后: 总容量: %s 已用: %s 剩余: %s 使用率: %s", $2, $3, $4, $5}'
log "--->如果没生效,请重启设备<---"
expquit 2
else
log "已经扩展过或者挂载分区过请删除分区或者重置重新操作或者联系作者sirpdboy"
expquit 1
fi
}
get_config() {
config_get target_function $1 target_function 1
config_get target_disk $1 target_disk 1
config_get_bool keep_config $1 keep_config 1
config_get format_type $1 format_type
}
autopart() {
config_load partexp
config_foreach get_config global
touch $LOCK
init_env
gen_log
uci -q set fstab.@global[0].anon_mount='0'
uci -q set fstab.@global[0].auto_mount='0'
uci commit fstab
[ -e "/etc/config/dockerd" ] && /etc/init.d/dockerd stop >/dev/null 2> /dev/null
DISK=$target_disk
NEXTPART=1
DISKSALL=$(get_all_disks)
DISK_COUNT=$(echo "$DISKSALL" | wc -l)
log "系统中检测到的硬盘数量: $DISK_COUNT"
log "硬盘信息列表:" $DISKSALL
SYSTEM_DISK=$(get_system_disk)
log "系统盘: "$SYSTEM_DISK
case "$SYSTEM_DISK" in
/dev/$DISK*)
fdisksave /dev/$DISK
log "此次执行操作功能:$target_function ,目标盘是系统盘:/dev/$DISK"
PARTITIONSUM=$(get_partition_number $DISK)
log "目标盘 $DISK 一共有分区数: $PARTITIONSUM个"
if [[ "$PARTITIONSUM" -gt 2 ]];then
FREE_SPACE=$(check_free_space $(basename $DISK))
log "目标盘 $DISK 有剩余空间: $FREE_SPACE Gb"
if [[ "$FREE_SPACE" -gt 2 ]]; then
NEXTPART=$(get_next_partition_number $DISK)
else
NEXTPART=$(get_last_partition_number $DISK)
fi
else
FREE_SPACE=$(check_free_space $(basename $DISK))
log "目标盘 $DISK 有剩余空间: $FREE_SPACE Gb"
if [[ "$FREE_SPACE" -gt 2 ]]; then
NEXTPART=$(get_next_partition_number $DISK)
else
log "目标盘 $SYSTEM_DISK 没有足够的剩余空间!"
expquit 1
fi
fi
;;
*)
log "此次执行操作功能:$target_function ,目标盘(非系统盘)是:/dev/$DISK"
PARTITIONSUM=$(get_partition_number $DISK)
log "目标盘 $DISK 一共有分区数: $PARTITIONSUM个"
if [[ "$PARTITIONSUM" -gt 1 ]];then
FREE_SPACE=$(check_free_space $(basename $DISK))
log "目标盘 $DISK 有剩余空间: $FREE_SPACE Gb"
[[ $FREE_SPACE -gt 2 ]] && NEXTPART=$(get_next_partition_number $DISK) || NEXTPART=$(get_last_partition_number $DISK)
else
NEXTPART=1
fi
;;
esac
log "定位到操作目标设备分区:/dev/$DISK$NEXTPART"
case "$DISK" in
vd*) fdiskB $DISK $NEXTPART;;
sd*) fdiskB $DISK $NEXTPART;;
nvme*) fdiskB $DISK p$NEXTPART;;
mmc*) fdiskB $DISK p$NEXTPART;;
*)
log "目标设备/dev/$DISK暂不支持请联系作者sirpdboy"
;;
esac
expquit 1
}
start() {
[ -f $LOCK ] && exit
[ x$xBOOT = x1 ] || autopart
rm -f $LOCK 2>/dev/null
}
stop () {
rm -f $LOCK 2>/dev/null
}
boot() {
xBOOT=1 start
}
expquit() {
rm -f $LOCK
uci -q set fstab.@global[0].anon_mount='1'
uci -q set fstab.@global[0].auto_mount='1'
uci commit fstab
[ -e "/etc/config/dockerd" ] && /etc/init.d/dockerd restart >/dev/null 2> /dev/null
sleep 2
[ $1 = 3 ] && log "重启中...\n" &&reboot
exit $1
}

View File

@@ -1,5 +0,0 @@
#!/bin/sh
chmod +x /etc/init.d/partexp >/dev/null 2>&1
[ `uci -q get partexp.global` ] || uci set partexp.global=global
rm -rf /tmp/luci-modulecache /tmp/luci-indexcache*
exit 0

View File

@@ -0,0 +1,809 @@
#!/bin/sh
#
# Copyright (C) 2021-2025 sirpdboy <herboy2008@gmail.com> https://github.com/sirpdboy/luci-app-partexp
#
# This is free software, licensed under the Apache License, Version 2.0 .
#
CONFIG="partexp"
LOCK=/var/lock/$CONFIG.lock
LOGD=/var/$CONFIG
LOG=$LOGD/bk$CONFIG.log
[ -d "$LOGD" ] || mkdir -p $LOGD
init_env() {
[ ! -f "$LOG" ] && echo " " > $LOG
}
gen_log() {
log " == 开始执行 == " | tee -a $LOG
}
log(){
echo -e " $(date +'%Y-%m-%d %H:%M:%S') $*" | tee -a $LOG
}
logn(){
echo -e " $*" | tee -a $LOG
}
load_uci_config() {
if [ -f "/etc/config/$CONFIG" ]; then
target_function=$(uci -q get $CONFIG.global.target_function)
target_disk=$(uci -q get $CONFIG.global.target_disk)
keep_config=$(uci -q get $CONFIG.global.keep_config)
format_type=$(uci -q get $CONFIG.global.format_type)
target_function="${target_function:-/opt}"
keep_config="${keep_config:-0}"
format_type="${format_type:-0}"
return 0
else
log "警告:配置文件不存在"
return 1
fi
}
is_disk_mounted() {
DISK=$1
if mount | grep -q "$DISK "; then
return 0
else
return 1
fi
}
mount_device() {
local DEVICE=$1
local MOUNT_POINT=$2
local TYPE=$3
if [ ! -e "$DEVICE" ]; then
log "设备 $DEVICE 不存在"
return 1
fi
if [ ! -d "$MOUNT_POINT" ]; then
log "挂载点 $MOUNT_POINT 不存在,正在创建..."
mkdir -p "$MOUNT_POINT"
if [ $? -ne 0 ]; then
log "无法创建挂载点 $MOUNT_POINT"
return 1
fi
fi
if mount | grep -q "$DEVICE"; then
log "设备 $DEVICE 已挂载到其他位置"
return 1
fi
if mount | grep -q "$MOUNT_POINT"; then
log "挂载点 $MOUNT_POINT 已被其他设备占用"
return 1
fi
log "正在挂载 $DEVICE 到 $MOUNT_POINT..."
mount $TYPE "$DEVICE" "$MOUNT_POINT" >/dev/null 2> /dev/null
if [ $? -eq 0 ]; then
log "挂载成功: $DEVICE -> $MOUNT_POINT"
return 0
else
log "挂载失败: $DEVICE -> $MOUNT_POINT"
return 1
fi
}
# 取消硬盘挂载
umount_disk() {
DISK=$1
MOUNT=''
eval $(block info "$DISK" | grep -o -e "MOUNT=\S*")
if [ "$MOUNT" ]; then
umount $DISK 2>/dev/null
if [ $? -eq 0 ]; then
log "取消挂载:$DISK成功"
else
log "取消挂载:$DISK失败,请手动操作"
fi
else
log "设备/dev/$DISK未挂载"
fi
}
# 从 block info 中提取指定字段的值
get_block() {
local DISK=$1
local TYPE=$2
local value
value=`mount | grep $DISK |awk -F $TYPE '{print $2}' |awk '{print $1}' | head -1`
echo $value
}
# 检查是否有共享挂载(如 Samba 或 NFS
check_shared_mount() {
DISK=$1
if [ -f /etc/config/samba ]; then
SHARED=$(grep -q "/dev/$DISK" /etc/config/samba)
if [ $? -eq 0 ]; then
log "检测到 Samba 共享挂载: /dev/$DISK"
return 0
fi
fi
if [ -f /etc/exports ]; then
SHARED=$(grep -q "/dev/$DISK" /etc/exports)
if [ $? -eq 0 ]; then
log "检测到 NFS 共享挂载: /dev/$DISK"
return 0
fi
fi
return 1
}
usamba(){
s=$1
[ -e "/etc/config/$s" ] && {
msum=$(grep -c "config sambashare" /etc/config/$s)
for i in $(seq 0 $((msum)));do
pdev=`uci -q get $s.@sambashare[$i].path `
[ "$pdev" = "$2" ] && {
uci delete $s.@sambashare[$i]
uci commit $s
log "分区/dev/$b被挂载$MOUNT共享使用删除$s共享成功"
sleep 5
}
done
}
if [ -f /etc/config/$s ]; then
sed -i "/\/dev\/$b/d" /etc/config/$s
/etc/init.d/$s restart
fi
if [ -f /etc/exports ]; then
sed -i "/\/dev\/$b/d" /etc/exports
/etc/init.d/nfs restart
fi
}
is_disk_partitioned() {
PARTITION_COUNT=$(fdisk -l /dev/$1 2>/dev/null | grep -E "^/dev/$2" | wc -l)
if [[ "$PARTITION_COUNT" -gt 0 ]]; then
echo 1
else
echo 0
fi
}
partednew(){
DISK=$1
parted -s /dev/$DISK mklabel gpt
parted -s /dev/$DISK mkpart primary ext4 1MiB -1
}
fdisknew(){
echo -e "n\np\n\n\n\nw" | fdisk /dev/$1 >/dev/null 2> /dev/null
}
fdisksave(){
echo -e "n\w" | fdisk /dev/$1 >/dev/null 2> /dev/null
}
# 格式化磁盘函数 DISK=/dev/sda1 ;TYPE=btrfs
format_disk() {
local DISK=$1
local TYPE=$2
[[ $TYPE == '0' || $TYPE == '' ]] && TYPE="ext4"
log "正在格式化 $DISK "
mkfs.$TYPE -F "$DISK" >/dev/null 2>/dev/null
if [ $? -eq 0 ]; then
log "格式化 $TYPE 成功 $DISK"
return 0
else
log "格式化 $TYPE 失败 $DISK"
return 1
fi
}
# 获取系统盘
get_system_disk() {
rom_dev=$(df -h | grep boot | awk '{print $1}' | head -1 )
[ -z ${rom_dev} ] && rom_dev=`df -P /boot 2>/dev/null | awk 'NR==2 {print $1}'`
[ -z ${rom_dev} ] && rom_dev=`blkid -t TYPE="squashfs" |awk -F : '{print $1}'`
[ -z ${rom_dev} ] && TDISK=`lsblk -l -o NAME,MOUNTPOINTS | grep -E '/rom'| grep -v '/rom/' | head -1 | awk '{print $1}'` && rom_dev="/dev/$TDISK"
if [ -n "$rom_dev" ]; then
case "$rom_dev" in
/dev/sd[a-z][0-9]*)
disk=$(echo "$rom_dev" | sed 's/[0-9]*$//')
;;
/dev/nvme[0-9]*n[0-9]*p[0-9]*)
disk=$(echo "$rom_dev" | sed 's/p[0-9]*$//')
;;
/dev/mmcblk[0-9]*p[0-9]*)
disk=$(echo "$rom_dev" | sed 's/p[0-9]*$//')
;;
/dev/vd[a-z][0-9]*)
disk=$(echo "$rom_dev" | sed 's/[0-9]*$//')
;;
/dev/mtdblock*)
disk="$rom_dev"
;;
/dev/ubiblock*)
disk="$rom_dev"
;;
/dev/root)
# 特殊处理 /dev/root
disk=$(lsblk -no PKNAME / 2>/dev/null)
if [ -n "$disk" ]; then
disk="/dev/$disk"
fi
;;
*)
# 尝试通用匹配
if echo "$rom_dev" | grep -qE '^/dev/[a-z]+[0-9]+'; then
disk=$(echo "$rom_dev" | sed 's/[0-9]*$//')
fi
;;
esac
fi
if [ -n "$disk" ] && [ ! -b "$disk" ]; then
disk=""
fi
echo "$disk"
}
get_all_disks() {
DISKS=`find /dev -regex '.*/\(sd[a-z]\|mmcblk[0-9]\+\|sata[a-z]\|nvme[0-9]\+n[0-9]\+\|vd[a-z]\)$'`
echo "$DISKS"
}
check_part_space() {
DISK=$1
info=$(lsblk -no SIZE,FSTYPE,MOUNTPOINT "$DISK" | awk '{print $1}')
if [ -z "$info" ]; then
echo "物理大小: 未知(可能是未格式化的裸分区)"
else
echo $info |awk -F '.' '{print $1}' | sed 's/[A-Za-z]//g'
fi
}
check_free_space() {
DISK=$1
PARTED_OUTPUT=$(parted -s /dev/$DISK unit GB print free 2>/dev/null)
FREE_SPACE=$(echo "$PARTED_OUTPUT" | grep "Free Space" | awk '{print $3}' )
echo $FREE_SPACE |awk -F '.' '{print $1}' | sed 's/[A-Za-z]//g'
}
show_partition_info() {
local partition="$1"
if [ ! -e "$partition" ]; then
logn "错误:分区 $partition 不存在!"
return 1
fi
logn " === 分区信息 [$partition] ==="
local lsblk_info=$(lsblk -no SIZE,FSTYPE,MOUNTPOINT "$partition" 2>/dev/null)
if [ -z "$lsblk_info" ]; then
logn "物理大小: 未知(可能是未格式化的裸分区)"
else
local size=$(echo "$lsblk_info" | awk '{print $1}')
local fstype=$(echo "$lsblk_info" | awk '{print $2}')
local mountpoint=$(echo "$lsblk_info" | awk '{print $3}')
logn "物理大小: $size 文件系统: ${fstype:-未知} 挂载点: ${mountpoint:-未挂载}"
fi
if df "$partition" &>/dev/null; then
local df_info=$(df -h "$partition" | tail -n 1 | awk 'NR=2 {print $2,$3,$4,$5}')
logn "[已挂载] 空间使用情况:"
logn "总容量: $(echo "$df_info" | awk '{print $1}') 已用: $(echo "$df_info" | awk '{print $2}') 剩余: $(echo "$df_info" | awk '{print $3}') 使用率: $(echo "$df_info" | awk '{print $4}')"
else
logn "[未挂载] 无法查询使用情况(需先挂载)"
fi
local disk="${partition%[0-9]*}"
local part_num="${partition##*[!0-9]}"
logn "分区表信息:"
logn `parted -s "$disk" unit MiB print | grep -w "^ $part_num" | awk '{print "起始: " $2 " MiB | 结束: " $3 " MiB | 类型: " $5}'`
}
get_next_partition_number() {
DISK=$1
PARTITIONS=$(fdisk -l /dev/$DISK 2>/dev/null | grep -v boot | grep -E "^/dev/$DISK" | awk '{print $1}' | sed 's/\/dev\/[a-z]*//g' | awk -F '[^0-9]+' '{print $NF}')
MAX_PARTITION=$(echo "$PARTITIONS" | sort -n | tail -n 1)
NEXT_PARTITION=$(awk -v n="$MAX_PARTITION" 'BEGIN { print n + 1 }')
echo "$NEXT_PARTITION"
}
get_last_partition_number() {
DISK=$1
PARTITIONS=$(fdisk -l /dev/$DISK 2>/dev/null | grep -v boot | grep -E "^/dev/$DISK" | awk '{print $1}' | sed 's/\/dev\/[a-z]*//g' | awk -F '[^0-9]+' '{print $NF}')
MAX_PARTITION=$(echo "$PARTITIONS" | sort -n | tail -n 1)
echo "$MAX_PARTITION"
}
get_partition_number() {
DISK=$1
PARTITIONS=$(fdisk -l /dev/$DISK 2>/dev/null | grep -v boot | grep -E "^/dev/$DISK" | awk '{print $1}' | sed 's/\/dev\/[a-z]*//g' | wc -l)
echo "$PARTITIONS"
}
rootpt_resize() {
if [ ! -e /etc/rootpt-resize ]; then
log "--->请稍侯,系统根分区扩展中<---"
ROOTBLK="$(readlink -f /sys/dev/block/"$(awk -e '$9="/dev/root"{print $3}' /proc/self/mountinfo)")"
ROOTDISK="/dev/$(basename "${ROOTBLK%/}")"
ROOTPART="${ROOTBLK##*[^0-9]}"
partplace=$(fdisk -l 2>/dev/null | grep "$ROOTDISK" | awk '{print $5}' )
log "--->根分区$ROOTDISK$partplace <---"
sleep 3
parted -f -s "${ROOTDISK}" resizepart "${ROOTPART}" 100%
mount_root done
touch /etc/rootpt-resize
sleep 3
log "--->系统根分区扩展成功!<---"
partplace=$(fdisk -l 2>/dev/null | grep "$ROOTDISK" | awk '{print $5}' )
log "--->根分区$ROOTDISK扩展后容量$partplace <---"
log "--->如果没生效,请重启设备<---"
expquit 2
else
log "已经扩展过或者挂载分区过请删除分区或者重置重新操作或者联系作者sirpdboy"
expquit 1
fi
}
rootfs_resize() {
if [ ! -e /etc/rootfs-resize ] && [ -e /etc/rootpt-resize ]; then
log "--->请稍侯,系统根分区扩展中<---"
ROOTBLK="$(readlink -f /sys/dev/block/"$(awk -e '$9="/dev/root"{print $3}' /proc/self/mountinfo)")"
ROOTPART="${ROOTBLK##*[^0-9]}"
ROOTDISK="/dev/$(basename "${ROOTBLK%/}")"
partplace=$(fdisk -l 2>/dev/null | grep "$ROOTDISK" | awk '{print $5}' )
log "--->根分区$ROOTDISK$partplace <---"
sleep 3
parted -f -s "${ROOTDISK}" resizepart "${ROOTPART}" 100%
mount_root done
touch /etc/rootpt-resize
sleep 3
log "--->系统根分区扩展成功!<---"
partplace=$(fdisk -l 2>/dev/null | grep "$ROOTDISK" | awk '{print $5}' )
log "--->根分区$ROOTDISK扩展后容量$partplace <---"
log "--->请稍侯系统overlay扩展中<---"
df -h /overlay | awk 'NR=2 {printf " overlay扩展前: 总容量: %s 已用: %s 剩余: %s 使用率: %s", $2, $3, $4, $5}'
LOOPDEV="$(awk -e '$5="/overlay"{print $9}' /proc/self/mountinfo)"
if [ -z "${LOOPDEV}" ]; then
LOOP_DEV="$(losetup -f)"
losetup "${LOOPDEV}" "${ROOTDEV}"
fi
FSTYPE=$(blkid -o value -s TYPE "$LOOPDEV" 2>/dev/null)
umount -l /overlay
mount -t tmpfs -o size=128M tmpfs /overlay
losetup -d /dev/loop0
losetup -fP ${ROOTDISK}
case "$FSTYPE" in
f2fs)
umount /overlay || { log "错误:无法卸载 /overlay"; expquit 1; }
fsck.f2fs -f "$LOOPDEV"
resize.f2fs -f "$LOOPDEV" || { log "错误f2fs 调整大小失败"; expquit 1; }
;;
ext4)
resize2fs -f "$LOOPDEV" || { log "错误ext4 调整大小失败"; expquit 1; }
;;
*)
log "--->分区格式 $FSTYPE 不识别overlay 扩展失败!<---"
expquit 1
;;
esac
mount_root done
touch /etc/rootfs-resize
sleep 3
log "--->系统overlay扩展成功<---"
df -h /overlay | awk 'NR=2 {printf " overlay扩展后: 总容量: %s 已用: %s 剩余: %s 使用率: %s", $2, $3, $4, $5}'
log "--->如果没生效,请重启设备<---"
expquit 2
else
log "已经扩展过或者挂载分区过请删除分区或者重置重新操作或者联系作者sirpdboy"
expquit 1
fi
}
get_config() {
config_get target_function $1 target_function 1
config_get target_disk $1 target_disk 1
config_get_bool keep_config $1 keep_config 1
config_get format_type $1 format_type
}
# 修改 fdiskB 函数,使用环境变量参数
fdiskB() {
a=$1
b=$1$2
log "开始检测目标$a信息"
log "检测/dev/$a是否需要分区和格式化"
block detect > /etc/config/fstab
uci -q set fstab.@global[0].anon_mount='0'
uci -q set fstab.@global[0].auto_mount='0'
uci commit fstab
if [ "$target_function" = '/opt' ]; then
/etc/init.d/dockerd stop >/dev/null 2> /dev/null
amount=`mount |grep /opt | awk '{print $1}'`
if [ -n "$amount" ]; then
umount $amount >/dev/null 2> /dev/null
log "取消/opt之前的挂载$amount成功"
fi
for OPT in $(mount |grep /opt | awk '{print $3}');do
umount $OPT >/dev/null 2> /dev/null
log "取消/opt之前的挂载$OPT成功"
done
fi
[ -d "/mnt/$b" ] || mkdir -p /mnt/$b
if is_disk_mounted "/dev/$b"; then
log "设备 /dev/$b 已挂载,尝试取消挂载..."
if check_shared_mount $b; then
usamba samba4 $MOUNT
usamba samba $MOUNT
sleep 5
fi
umount_disk "/dev/$b"
[ $? -ne 0 ] || umount_disk "/mnt/$b"
else
log "设备/dev/$b未挂载"
isfdisk=0
isP=$(is_disk_partitioned $a $b)
if [ "$isP" = '0' ]; then
fdisksave $a
fdisknew $a
sleep 2
isfdisk=1
fi
isP=$(is_disk_partitioned $a $b)
if [[ "$isP" = '1' && "$isfdisk" = 1 ]]; then
log "分区$b建立成功"
elif [[ "$isP" = '1' && "$isfdisk" = 0 ]]; then
log "检测目标分区$b已存在."
else
log "分区$b建立失败请检查$b硬盘空间"
expquit 1
fi
sleep 1
fi
if is_disk_mounted "/dev/$b"; then
umount /dev/$b >/dev/null 2> /dev/null
[ $? -ne 0 ] && block umount /dev/$b >/dev/null 2> /dev/null
fi
sleep 5
if [[ "$target_function" = "/" || "$target_function" = "/overlay" ]]; then
format_disk "/dev/$b" $format_type
elif [[ "$format_type" != "0" || "$isfdisk" = "1" ]]; then
format_disk "/dev/$b" $format_type
else
log "设备/dev/$b如果未格式化,可能无法正常使用."
fi
TYPE=''
eval $(blkid "/dev/$b" | grep -o -e "TYPE=\S*")
log "检测设备/dev/$b分区$TYPE格式"
sleep 1
if [ "$TYPE" = "ntfs" ]; then
if [ `which ntfs-3g` ]; then
if is_disk_mounted "/mnt/$b"; then
mount_device /dev/$b /mnt/$b "-t ntfs-3g"
fi
else
if is_disk_mounted "/mnt/$b"; then
mount_device /dev/$b /mnt/$b "-t ntfs3"
fi
fi
else
mount /dev/$b /mnt/$b >/dev/null 2> /dev/null
fi
sleep 1
UUID=''
eval $(block info /dev/$b | grep -o -e "UUID=\S*")
if [ ! "$UUID" ]; then
log "获取/dev/$b设备UUID信息失败!"
expquit 1
else
log "获取/dev/$b设备UUID信息成功!"
fi
sleep 1
case "$target_function" in
"/overlay")
if [ "$keep_config" = "1" ]; then
tar -C /overlay -cvf - . | tar -C /mnt/$b/ -xf - || tar -C /rom/overlay -cvf - . | tar -C /mnt/$b/ -xf -
umount /dev/$b >/dev/null 2> /dev/null
[ $? -ne 0 ] && umount /mnt/$b >/dev/null 2> /dev/null
[ $? -ne 0 ] && block umount /dev/$b >/dev/null 2> /dev/null
block detect > /etc/config/fstab
OVERLAY=`uci -q get fstab.@mount[0].target`
if [[ "$OVERLAY" = "/overlay" || "$OVERLAY" = "/dev/loop0" ]]; then
uci -q set fstab.@mount[0].uuid="${UUID}"
uci -q set fstab.@mount[0].target='/overlay'
uci -q set fstab.@mount[0].enabled='0'
fi
msum=$(grep -c "'mount'" /etc/config/fstab)
for i in $(seq 0 $((msum-1)))
do
zuuid=`uci -q get fstab.@mount[$i].uuid`
[ $? -ne 0 ] && break
if [ "$zuuid" = "$UUID" ]; then
uci -q set fstab.@mount[$i].target="/overlay"
uci -q set fstab.@mount[$i].enabled='1'
fi
done
uci set fstab.@global[0].delay_root="15"
uci commit fstab
log "保留数据overlay扩展/dev/$b成功"
eval $(block info /dev/$b | grep -o -e "MOUNT=\S*")
echo $MOUNT $a> /etc/partexppath
log "扩容成功!可直接【在线升级】扩容升级了!"
show_partition_info /dev/$b
sleep 3
expquit 2
else
umount /dev/$b >/dev/null 2> /dev/null
[ $? -ne 0 ] && umount /mnt/$b >/dev/null 2> /dev/null
[ $? -ne 0 ] && block umount /dev/$b >/dev/null 2> /dev/null
block detect > /etc/config/fstab
OVERLAY=`uci -q get fstab.@mount[0].target`
if [[ "$OVERLAY" = "/overlay" || "$OVERLAY" = "/dev/loop0" ]]; then
uci -q set fstab.@mount[0].uuid="${UUID}"
uci -q set fstab.@mount[0].target='/overlay'
uci -q set fstab.@mount[0].enabled='0'
fi
msum=$(grep -c "'mount'" /etc/config/fstab)
for i in $(seq 0 $((msum-1)))
do
zuuid=`uci -q get fstab.@mount[$i].uuid`
[ $? -ne 0 ] && break
if [ "$zuuid" = "$UUID" ]; then
uci -q set fstab.@mount[$i].target="/overlay"
uci -q set fstab.@mount[$i].enabled='1'
fi
done
uci set fstab.@global[0].delay_root="15"
uci commit fstab
log "不保留数据overlay扩展/dev/$b成功"
eval $(block info /dev/$b | grep -o -e "MOUNT=\S*")
echo $MOUNT $a> /etc/partexppath
log "扩容成功!可直接【在线升级】扩容升级了!"
show_partition_info /dev/$b
sleep 3
expquit 2
fi
;;
"/opt")
umount /dev/$b >/dev/null 2> /dev/null
[ $? -ne 0 ] && umount /mnt/$b >/dev/null 2> /dev/null
[ $? -ne 0 ] && block umount /dev/$b >/dev/null 2> /dev/null
block detect > /etc/config/fstab
mkdir -p $target_function
msum=$(grep -c "'mount'" /etc/config/fstab)
mount_device /dev/$b "$target_function"
for i in $(seq 0 $((msum-1)))
do
zuuid=`uci -q get fstab.@mount[$i].uuid`
[ $? -ne 0 ] && break
if [ "$zuuid" = "$UUID" ]; then
uci -q set fstab.@mount[$i].target="$target_function"
uci -q set fstab.@mount[$i].enabled='1'
fi
done
uci commit fstab
if is_disk_mounted "/opt"; then
log "/dev/$b分区扩容和挂载到$target_function成功"
eval $(block info /dev/$b | grep -o -e "MOUNT=\S*")
echo $MOUNT $a> /etc/partexppath
log "扩容成功!可直接【在线升级】扩容升级了!"
show_partition_info /dev/$b
expquit 2
else
log "/dev/$b分区扩容和挂载到$target_function失败"
fi
;;
"/")
ROOTBLK="$(readlink -f /sys/dev/block/"$(awk '$9="/dev/root"{print $3}' /proc/self/mountinfo)")"
[ -z "$ROOTBLK" ] && { log "错误:无法获取根分区块设备"; expquit 1; }
ROOTDISK="/dev/$(basename "${ROOTBLK%/}")"
FSTYPE=$(blkid -o value -s TYPE "$ROOTDISK" 2>/dev/null)
if [[ "$FSTYPE" != "squashfs" && -n "$FSTYPE" ]]; then
if [ "$target_function" = '/' ]; then
FREE_SPACE=$(check_free_space $(basename $DISK))
log "目标盘 $ROOT_PART $FSTYPE有剩余空间: $FREE_SPACE Gb"
if [[ "$FREE_SPACE" -gt 2 ]]; then
rootpt_resize
expquit 2
else
log "目标盘 $SYSTEM_DISK $FSTYPE没有足够的剩余空间"
expquit 1
fi
fi
if [ "$target_function" = '/overlay' ]; then
FREE_SPACE=$(check_free_space $(basename $DISK))
log "目标盘 $ROOT_PART $FSTYPE有剩余空间: $FREE_SPACE Gb"
if [[ "$FREE_SPACE" -gt 2 ]]; then
rootfs_resize
eval $(block info /dev/$b | grep -o -e "MOUNT=\S*")
log "/dev/$b分区扩容和挂载到$target_function成功"
echo $MOUNT $a> /etc/partexppath
log "扩容成功!可直接【在线升级】扩容升级了!"
show_partition_info /dev/$b
expquit 2
else
log "目标盘 $SYSTEM_DISK $FSTYPE没有足够的剩余空间"
expquit 1
fi
fi
else
log "目标硬盘不支持/根分区扩展请换EXT4固件"
fi
sleep 3
expquit 2
;;
*)
umount /dev/$b >/dev/null 2> /dev/null
[ $? -ne 0 ] && umount /mnt/$b >/dev/null 2> /dev/null
[ $? -ne 0 ] && block umount /dev/$b >/dev/null 2> /dev/null
block detect > /etc/config/fstab
mkdir -p /mnt/$b
msum=$(grep -c "'mount'" /etc/config/fstab)
mount_device /dev/$b /mnt/$b
for i in $(seq 0 $((msum-1)))
do
zuuid=`uci -q get fstab.@mount[$i].uuid`
[ $? -ne 0 ] && break
if [ "$zuuid" = "$UUID" ]; then
uci -q set fstab.@mount[$i].target="/mnt/$b"
uci -q set fstab.@mount[$i].enabled='1'
fi
done
uci commit fstab
if is_disk_mounted /mnt/$b; then
log "/dev/$b分区扩容和挂载到/mnt/$b成功"
eval $(block info /dev/$b | grep -o -e "MOUNT=\S*")
echo $MOUNT $a> /etc/partexppath
log "扩容成功!可直接【在线升级】扩容升级了!"
show_partition_info /dev/$b
expquit 2
else
log "/dev/$b分区扩容和挂载到/mnt/$b失败"
fi
;;
esac
}
autopart() {
[ -f $LOCK ] && expquit 1
if [ -f "/etc/config/$CONFIG" ]; then
target_function=$(uci -q get partexp.global.target_function)
target_disk=$(uci -q get partexp.global.target_disk)
keep_config=$(uci -q get partexp.global.keep_config)
format_type=$(uci -q get partexp.global.format_type)
export target_function="${target_function:-/opt}"
export keep_config="${keep_config:-0}"
export format_type="${format_type:-0}"
export target_disk="${target_disk:-sda}"
fi
touch $LOCK
init_env
gen_log
uci -q set fstab.@global[0].anon_mount='0'
uci -q set fstab.@global[0].auto_mount='0'
uci commit fstab
[ -e "/etc/config/dockerd" ] && /etc/init.d/dockerd stop >/dev/null 2> /dev/null
DISK=${target_disk}
NEXTPART=1
DISKSALL=$(get_all_disks)
DISK_COUNT=$(echo "$DISKSALL" | wc -l)
log "系统中检测到的硬盘数量: $DISK_COUNT"
log "硬盘信息列表:" $DISKSALL
SYSTEM_DISK=$(get_system_disk)
log "系统盘: "$SYSTEM_DISK
case "$SYSTEM_DISK" in
/dev/$DISK*)
fdisksave /dev/$DISK
log "操作功能:$target_function ,系统盘:/dev/$DISK"
PARTITIONSUM=$(get_partition_number $DISK)
log "目标盘 $DISK 一共有分区数: $PARTITIONSUM个"
if [[ "$PARTITIONSUM" -gt 2 ]]; then
FREE_SPACE=$(check_free_space $(basename $DISK))
log "目标盘 $DISK 有剩余空间: $FREE_SPACE Gb"
if [[ "$FREE_SPACE" -gt 2 ]]; then
NEXTPART=$(get_next_partition_number $DISK)
else
NEXTPART=$(get_last_partition_number $DISK)
fi
else
FREE_SPACE=$(check_free_space $(basename $DISK))
log "目标盘 $DISK 有剩余空间: $FREE_SPACE Gb"
if [[ "$FREE_SPACE" -gt 2 ]]; then
NEXTPART=$(get_next_partition_number $DISK)
else
log "目标盘 $SYSTEM_DISK 没有足够的剩余空间!"
expquit 1
fi
fi
;;
*)
log "操作功能:$target_function ,非系统盘:/dev/$DISK"
PARTITIONSUM=$(get_partition_number $DISK)
log "目标盘 $DISK 一共有分区数: $PARTITIONSUM个"
if [[ "$PARTITIONSUM" -gt 1 ]]; then
FREE_SPACE=$(check_free_space $(basename $DISK))
log "目标盘 $DISK 有剩余空间: $FREE_SPACE Gb"
[[ $FREE_SPACE -gt 2 ]] && NEXTPART=$(get_next_partition_number $DISK) || NEXTPART=$(get_last_partition_number $DISK)
else
NEXTPART=1
fi
;;
esac
log "定位到操作目标设备分区:/dev/$DISK$NEXTPART"
case "$DISK" in
vd*|sd*) fdiskB $DISK $NEXTPART;;
nvme*|mmc*) fdiskB $DISK p$NEXTPART;;
# mtdblock*|ubiblock*) fdiskB $DISK $NEXTPART;;
*)
log "目标设备/dev/$DISK暂不支持请联系作者sirpdboy"
;;
esac
expquit 1
}
stop() {
rm -f $LOCK 2>/dev/null
}
expquit() {
rm -f $LOCK
uci -q set fstab.@global[0].anon_mount='1'
uci -q set fstab.@global[0].auto_mount='1'
uci commit fstab
[ -e "/etc/config/dockerd" ] && /etc/init.d/dockerd restart >/dev/null 2> /dev/null
[ "$1" = "2" ] && log "如果没生效,请重启设备"
sleep 1
log "== 操作完成 =="
[ "$1" = "3" ] && log "重启中...\n" && reboot
exit $1
}
case "$1" in
autopart)
"$1"
;;
*)
echo "Usage: $0 {autopart}"
exit 1
;;
esac

View File

@@ -0,0 +1,228 @@
#!/bin/sh
. /usr/share/libubox/jshn.sh
# 分区扩展服务
case "$1" in
list)
echo '{
"autopart": {
"description": "Execute automatic partition expansion"
},
"get_log": {
"description": "Get operation log",
"arguments": {
"position": "string"
}
},
"get_devices": {
"description": "Get available disk devices"
},
"get_status": {
"description": "Get operation status"
},
"save_config": {
"description": "Save configuration",
"arguments": {
"target_function": "string",
"target_disk": "string",
"keep_config": "string",
"format_type": "string"
}
}
}'
;;
call)
case "$2" in
autopart)
if [ -f "/var/run/partexp.lock" ]; then
echo '{"error": "Another operation is in progress"}'
return 1
fi
# 检查配置文件是否存在
if [ ! -f "/etc/config/partexp" ]; then
echo '{"error": "Configuration file not found. Please save settings first."}'
return 1
fi
# 创建锁文件
touch /var/run/partexp.lock
# 执行分区操作(后台异步执行)
{
# 清空日志文件
echo "" > /tmp/partexp.log
# 调用原 partexp 脚本
/usr/bin/partexp autopart > /tmp/partexp.log 2>&1
# 清理锁文件
rm -f /var/run/partexp.lock
} &
echo '{"success": true, "pid": "'$!'", "message": "Partition expansion started"}'
;;
get_log)
# 获取操作日志
read input
json_load "$input"
json_get_vars position
if [ ! -f "/tmp/partexp.log" ]; then
echo '{"log": "", "complete": true}'
return 0
fi
if [ -z "$position" ] || [ "$position" = "0" ]; then
# 从头读取
log_content=$(cat /tmp/partexp.log 2>/dev/null | tail -c 2048000)
new_position=$(stat -c%s /tmp/partexp.log 2>/dev/null || echo "0")
json_init
json_add_string "log" "$log_content"
json_add_boolean "complete" false
json_add_string "position" "$new_position"
json_dump
else
# 从指定位置读取
position=${position:-0}
file_size=$(stat -c%s /tmp/partexp.log 2>/dev/null || echo 0)
if [ "$position" -lt "$file_size" ]; then
log_content=$(tail -c +$((position + 1)) /tmp/partexp.log | head -c 2048000)
new_position=$((position + ${#log_content}))
json_init
json_add_string "log" "$log_content"
json_add_boolean "complete" false
json_add_string "position" "$new_position"
json_dump
else
json_init
json_add_string "log" ""
json_add_boolean "complete" true
json_add_string "position" "$position"
json_dump
fi
fi
;;
get_devices)
# 获取可用设备列表 - 仅磁盘
json_init
json_add_array "devices"
# 所有可能的磁盘设备模式
disk_patterns="
^sd[a-z]$
^mmcblk[0-9]+$
^nvme[0-9]+n[0-9]+$
^vd[a-z]$
^hd[a-z]$
^xvd[a-z]$
^ubd[a-z]+$
^dasd[a-z]+$
^cciss[0-9]+$
^ida[0-9]+$
^rd[0-9]+$
^mtdblock[0-9]+$
^nbd[0-9]+$
^zram[0-9]+$
"
for dev in /sys/class/block/*; do
dev_name=$(basename "$dev")
# 跳过分区、loop、dm 设备
if [ -f "$dev/partition" ] || [ -d "$dev/loop" ] || [ -d "$dev/dm" ]; then
continue
fi
# 检查是否为磁盘设备
is_disk=0
for pattern in $disk_patterns; do
if echo "$dev_name" | grep -qE "$pattern"; then
is_disk=1
break
fi
done
if [ $is_disk -eq 1 ]; then
size="0"
if [ -f "$dev/size" ]; then
size_sectors=$(cat "$dev/size")
size=$((size_sectors / 2048))
fi
json_add_object
json_add_string "name" "$dev_name"
json_add_string "dev" "/dev/$dev_name"
json_add_int "size" "$size"
json_close_object
fi
done
json_close_array
json_dump
;;
get_status)
# 获取操作状态
if [ -f "/var/run/partexp.lock" ]; then
echo '{"running": true}'
else
echo '{"running": false}'
fi
;;
save_config)
# 保存配置
read input
json_load "$input"
json_get_vars target_function target_disk keep_config format_type
# 验证参数
if [ -z "$target_function" ]; then
echo '{"error": "Missing target_function parameter"}'
return 1
fi
# 设置默认值
target_disk="${target_disk:-}"
keep_config="${keep_config:-0}"
format_type="${format_type:-0}"
# 构建配置内容
CONFIG_FILE="/etc/config/partexp"
mkdir -p "$(dirname "$CONFIG_FILE")"
cat > "$CONFIG_FILE" << EOF
# Auto-generated by partexp
config global global
option target_function '$target_function'
option target_disk '$target_disk'
option keep_config '$keep_config'
option format_type '$format_type'
EOF
if [ $? -eq 0 ]; then
echo '{"success": true, "message": "Configuration saved"}'
else
echo '{"error": "Failed to save configuration"}'
return 1
fi
;;
*)
echo '{"error": "Method not found"}'
;;
esac
;;
*)
echo '{"error": "Invalid action"}'
;;
esac

View File

@@ -0,0 +1,14 @@
{
"admin/system/partexp": {
"title": "Partition Expansion",
"order": 54,
"action": {
"type": "view",
"path": "partexp"
},
"acl": ["luci-app-partexp"],
"depends": {
"acl": ["luci-app-partexp"]
}
}
}

View File

@@ -2,22 +2,18 @@
"luci-app-partexp": {
"description": "Grant UCI access for luci-app-partexp",
"read": {
"file": {
"/etc/init.d/partexp": [ "exec" ],
"/var/partexp": ["read"],
"/etc/partexppath": ["read"],
"/etc/partexp/": ["read"]
},
"uci": [ "partexp" ]
"ubus": {
"file": ["exec", "list", "stat", "read"],
"uci": [ "*" ],
"partexp": ["*"]
}
},
"write": {
"file": {
"/var/partexp": ["write"],
"/etc/partexppath": ["write"],
"/etc/partexp/": ["write"]
},
"uci": [ "partexp" ]
}
"ubus": {
"partexp": ["*"],
"file": ["write"],
"uci": ["*"]
}
}
}
}
}

View File

@@ -8,7 +8,7 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=lucky
PKG_VERSION:=2.20.2
PKG_VERSION:=2.24.0
PKG_RELEASE:=1
PKGARCH:=all
@@ -45,7 +45,7 @@ PKG_LICENSE_FILES:=LICENSE
PKG_MAINTAINER:=GDY666 <gdy666@foxmail.com>
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)
PKG_HASH:=544d800e234d9348870edfdaf615a068779674ed1731beb4ff73e00376b15121
PKG_HASH:=d7409d3ab8742e2b7fa39339fe3b10edb969e01e177ea5c6f2c8b1776ce2593c
include $(INCLUDE_DIR)/package.mk