diff --git a/luci-app-partexp/luci-app-partexp/Makefile b/luci-app-partexp/luci-app-partexp/Makefile index c9e4e73bd..1d5ada132 100644 --- a/luci-app-partexp/luci-app-partexp/Makefile +++ b/luci-app-partexp/luci-app-partexp/Makefile @@ -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 -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 diff --git a/luci-app-partexp/luci-app-partexp/README.md b/luci-app-partexp/luci-app-partexp/README.md deleted file mode 100644 index 35356edcf..000000000 --- a/luci-app-partexp/luci-app-partexp/README.md +++ /dev/null @@ -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耗费大量精力制作测试,请勿删除制作者信息!! - - - -- [partexp](#luci-app-partexp) - - [特性](#特性) - - [使用方法](#使用方法) - - [说明](#说明) - - [界面](#界面) - - [捐助](#捐助) - - - -## 版本 - -- 最新更新版本号: 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) - -| 图飞了😂 | 图飞了😂 | -| :-----------------: | :-------------: | -|![xm1](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/支付宝.png) | ![xm1](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/微信.png) | - - - 图飞了😂 - diff --git a/luci-app-partexp/luci-app-partexp/htdocs/luci-static/resources/view/partexp.js b/luci-app-partexp/luci-app-partexp/htdocs/luci-static/resources/view/partexp.js new file mode 100644 index 000000000..79e521104 --- /dev/null +++ b/luci-app-partexp/luci-app-partexp/htdocs/luci-static/resources/view/partexp.js @@ -0,0 +1,963 @@ +/* + * Copyright (C) 2022-2025 Sirpdboy + * + * 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 = [ + '', + '

' + _('One click partition expansion mounting tool') + '

', + '
', + '
', + ' ' + _('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.') + '
', + ' ' + _('For specific usage, see:') + ' ', + ' ', + ' GitHub @partexp', + ' ', + '
', + '
', + '
', + '
', + '
', + '
', + '
', + ' ', + '
', + ' ', + '
' + _('Select the function to be performed') + '
', + '
', + '
', + '
', + ' ', + '
', + ' ', + '
' + _('Select the hard disk device to operate') + '
', + '
', + '
', + '
', + ' ', + '
', + ' ', + ' ', + '
', + '
', + '
', + ' ', + '
', + ' ', + '
', + '
', + '
', + ' ', + '
', + ' ', + '
', + '
', + '
', + '
', + '
', + '
', + ' ', + '
', + '
', + '
', + '
0%
', + '
', + '
', + '
', + '
', + '
', + '
', + '
', + ' ', + '
', + ' ', + '
', + '
', + '
', + '
' + ]; + 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 +}); \ No newline at end of file diff --git a/luci-app-partexp/luci-app-partexp/luasrc/controller/partexp.lua b/luci-app-partexp/luci-app-partexp/luasrc/controller/partexp.lua deleted file mode 100644 index 7fb671da9..000000000 --- a/luci-app-partexp/luci-app-partexp/luasrc/controller/partexp.lua +++ /dev/null @@ -1,50 +0,0 @@ ---[[ -LuCI - Lua Configuration Partition Expansion - Copyright (C) 2022-2025 sirpdboy 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 diff --git a/luci-app-partexp/luci-app-partexp/luasrc/model/cbi/partexp/global.lua b/luci-app-partexp/luci-app-partexp/luasrc/model/cbi/partexp/global.lua deleted file mode 100644 index 5d985cb1a..000000000 --- a/luci-app-partexp/luci-app-partexp/luasrc/model/cbi/partexp/global.lua +++ /dev/null @@ -1,79 +0,0 @@ ---[[ -LuCI - Lua Configuration Interface - Copyright (C) 2022-2025 sirpdboy 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", "" .. translate("One click partition expansion mounting tool") .."", -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.
For specific usage, see:") ..translate("GitHub @sirpdboy:luci-app-partexp") ) - -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 diff --git a/luci-app-partexp/luci-app-partexp/luasrc/view/partexp.htm b/luci-app-partexp/luci-app-partexp/luasrc/view/partexp.htm deleted file mode 100644 index eb2247699..000000000 --- a/luci-app-partexp/luci-app-partexp/luasrc/view/partexp.htm +++ /dev/null @@ -1,130 +0,0 @@ -<%# -Copyright (C) 2022-2024 sirpdboy https://github.com/sirpdboy/partexp --%> -<%+cbi/valueheader%> -<%local fs=require"nixio.fs"%> - - - -<%+cbi/valuefooter%> diff --git a/luci-app-partexp/luci-app-partexp/luasrc/view/partexplog.htm b/luci-app-partexp/luci-app-partexp/luasrc/view/partexplog.htm deleted file mode 100644 index b2b7b60f5..000000000 --- a/luci-app-partexp/luci-app-partexp/luasrc/view/partexplog.htm +++ /dev/null @@ -1,16 +0,0 @@ -<%+cbi/valueheader%> - - - -<%+cbi/valuefooter%> diff --git a/luci-app-partexp/luci-app-partexp/luasrc/view/partexplogrun.htm b/luci-app-partexp/luci-app-partexp/luasrc/view/partexplogrun.htm deleted file mode 100644 index f42d6339f..000000000 --- a/luci-app-partexp/luci-app-partexp/luasrc/view/partexplogrun.htm +++ /dev/null @@ -1,54 +0,0 @@ -<%+cbi/valueheader%> - -<%:Reverse%> - - - -<%+cbi/valuefooter%> \ No newline at end of file diff --git a/luci-app-partexp/luci-app-partexp/po/templates/partexp.po b/luci-app-partexp/luci-app-partexp/po/templates/partexp.po new file mode 100644 index 000000000..a621753ed --- /dev/null +++ b/luci-app-partexp/luci-app-partexp/po/templates/partexp.po @@ -0,0 +1,199 @@ +# Translation file for partexp.js +# Copyright (C) 2022-2025 Sirpdboy +# 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 "" \ No newline at end of file diff --git a/luci-app-partexp/luci-app-partexp/po/zh-cn/partexp.po b/luci-app-partexp/luci-app-partexp/po/zh-cn/partexp.po deleted file mode 100644 index 48d259e93..000000000 --- a/luci-app-partexp/luci-app-partexp/po/zh-cn/partexp.po +++ /dev/null @@ -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.
For specific usage, see:" -msgstr "自动对目标设备分区格式化挂载,如果有多分区建议手动删除所有分区再使用本工具.
使用说明见:" - -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 "逆序" - - diff --git a/luci-app-partexp/luci-app-partexp/po/zh_Hans/partexp.po b/luci-app-partexp/luci-app-partexp/po/zh_Hans/partexp.po index 48d259e93..c12344a91 100644 --- a/luci-app-partexp/luci-app-partexp/po/zh_Hans/partexp.po +++ b/luci-app-partexp/luci-app-partexp/po/zh_Hans/partexp.po @@ -1,46 +1,72 @@ +# Translation file for partexp.js +# Copyright (C) 2022-2025 Sirpdboy +# 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.
For specific usage, see:" -msgstr "自动对目标设备分区格式化挂载,如果有多分区建议手动删除所有分区再使用本工具.
使用说明见:" +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 "加载配置失败" \ No newline at end of file diff --git a/luci-app-partexp/luci-app-partexp/root/etc/config/partexp b/luci-app-partexp/luci-app-partexp/root/etc/config/partexp index 8ec8397ad..d2709c878 100644 --- a/luci-app-partexp/luci-app-partexp/root/etc/config/partexp +++ b/luci-app-partexp/luci-app-partexp/root/etc/config/partexp @@ -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' diff --git a/luci-app-partexp/luci-app-partexp/root/etc/init.d/partexp b/luci-app-partexp/luci-app-partexp/root/etc/init.d/partexp deleted file mode 100644 index 806a19bf1..000000000 --- a/luci-app-partexp/luci-app-partexp/root/etc/init.d/partexp +++ /dev/null @@ -1,751 +0,0 @@ -#!/bin/sh /etc/rc.common - -# -# Copyright (C) 2021-2025 sirpdboy 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 -} - diff --git a/luci-app-partexp/luci-app-partexp/root/etc/partexp/lucilogpos b/luci-app-partexp/luci-app-partexp/root/etc/partexp/lucilogpos deleted file mode 100644 index a76eb9d7a..000000000 --- a/luci-app-partexp/luci-app-partexp/root/etc/partexp/lucilogpos +++ /dev/null @@ -1 +0,0 @@ -1043 \ No newline at end of file diff --git a/luci-app-partexp/luci-app-partexp/root/etc/partexp/partexp.log b/luci-app-partexp/luci-app-partexp/root/etc/partexp/partexp.log deleted file mode 100644 index 0ea660181..000000000 --- a/luci-app-partexp/luci-app-partexp/root/etc/partexp/partexp.log +++ /dev/null @@ -1 +0,0 @@ - 1 \ No newline at end of file diff --git a/luci-app-partexp/luci-app-partexp/root/etc/uci-defaults/40_luci-partexp b/luci-app-partexp/luci-app-partexp/root/etc/uci-defaults/40_luci-partexp deleted file mode 100644 index 9db7cc494..000000000 --- a/luci-app-partexp/luci-app-partexp/root/etc/uci-defaults/40_luci-partexp +++ /dev/null @@ -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 diff --git a/luci-app-partexp/luci-app-partexp/root/usr/bin/partexp b/luci-app-partexp/luci-app-partexp/root/usr/bin/partexp new file mode 100644 index 000000000..a58d03851 --- /dev/null +++ b/luci-app-partexp/luci-app-partexp/root/usr/bin/partexp @@ -0,0 +1,809 @@ +#!/bin/sh + +# +# Copyright (C) 2021-2025 sirpdboy 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 \ No newline at end of file diff --git a/luci-app-partexp/luci-app-partexp/root/usr/libexec/rpcd/partexp b/luci-app-partexp/luci-app-partexp/root/usr/libexec/rpcd/partexp new file mode 100644 index 000000000..e8ba056ab --- /dev/null +++ b/luci-app-partexp/luci-app-partexp/root/usr/libexec/rpcd/partexp @@ -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 \ No newline at end of file diff --git a/luci-app-partexp/luci-app-partexp/root/usr/share/luci/menu.d/luci-app-partexp.json b/luci-app-partexp/luci-app-partexp/root/usr/share/luci/menu.d/luci-app-partexp.json new file mode 100644 index 000000000..39fdd8158 --- /dev/null +++ b/luci-app-partexp/luci-app-partexp/root/usr/share/luci/menu.d/luci-app-partexp.json @@ -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"] + } + } +} diff --git a/luci-app-partexp/luci-app-partexp/root/usr/share/rpcd/acl.d/luci-app-partexp.json b/luci-app-partexp/luci-app-partexp/root/usr/share/rpcd/acl.d/luci-app-partexp.json index 900343401..20bf3550c 100644 --- a/luci-app-partexp/luci-app-partexp/root/usr/share/rpcd/acl.d/luci-app-partexp.json +++ b/luci-app-partexp/luci-app-partexp/root/usr/share/rpcd/acl.d/luci-app-partexp.json @@ -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": ["*"] + } } -} + } +} \ No newline at end of file diff --git a/lucky/Makefile b/lucky/Makefile index 20c54b4eb..2cb9b7bd5 100644 --- a/lucky/Makefile +++ b/lucky/Makefile @@ -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 PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION) -PKG_HASH:=544d800e234d9348870edfdaf615a068779674ed1731beb4ff73e00376b15121 +PKG_HASH:=d7409d3ab8742e2b7fa39339fe3b10edb969e01e177ea5c6f2c8b1776ce2593c include $(INCLUDE_DIR)/package.mk