Compare commits
10 Commits
353a9cb449
...
c9c8576401
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9c8576401 | ||
|
|
85be19a06e | ||
|
|
5ae8d48261 | ||
|
|
02b3697978 | ||
|
|
b2cf655dfb | ||
|
|
f0ebeb1b2d | ||
|
|
97a98b9774 | ||
|
|
7fa2d96855 | ||
|
|
334821751e | ||
|
|
bdf83f705b |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
rclone.exe
|
rclone.exe
|
||||||
md5_files/
|
md5_files/
|
||||||
combined_files/
|
combined_files/
|
||||||
|
man_files/
|
||||||
|
|||||||
@@ -57,6 +57,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<div id="file-selector-container" style="margin-bottom: 20px; background: white; padding: 15px; border-radius: 12px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); display: flex; align-items: center; gap: 15px;">
|
||||||
|
<button id="btnSelectDir" class="btn btn-primary" onclick="pickDirectory()"></button>
|
||||||
|
<span id="t-label-files" style="font-weight: 600; color: #636e72;"></span>
|
||||||
|
<select id="fileDropdown" style="flex-grow: 1; padding: 8px 12px; border-radius: 8px; border: 1px solid #ddd; outline: none; cursor: pointer;" onchange="loadSelectedFile(this.value)">
|
||||||
|
<option value="" disabled selected id="t-select-placeholder"></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="drop-zone">
|
<div id="drop-zone">
|
||||||
<p id="t-drop-hint"></p>
|
<p id="t-drop-hint"></p>
|
||||||
<span id="t-click-hint" style="color: #95a5a6;"></span>
|
<span id="t-click-hint" style="color: #95a5a6;"></span>
|
||||||
@@ -86,8 +94,8 @@
|
|||||||
<script>
|
<script>
|
||||||
// 内置默认语言
|
// 内置默认语言
|
||||||
const defaultI18n = {
|
const defaultI18n = {
|
||||||
zh: { name: "简体中文", dropHint: "📂 <b>拖入</b>生成的文件到这里", clickHint: "或者 点击此处手动选择", labelMatch: "一致 (Match)", labelMismatch: "损坏 (Mismatch)", labelMissing: "缺失 (Missing)", labelExtra: "多余 (Extra)", btnExpand: "全部展开", btnCollapse: "全部收起", modeFull: "显示模式:完整清单", modeError: "显示模式:只看异常", loadSuccess: "✅ 已加载:", files: " 个文件" },
|
zh: { name: "简体中文", dropHint: "📂 <b>拖入</b>生成的文件到这里", clickHint: "或者 点击此处手动选择", labelMatch: "一致 (Match)", labelMismatch: "损坏 (Mismatch)", labelMissing: "缺失 (Missing)", labelExtra: "多余 (Extra)", btnExpand: "全部展开", btnCollapse: "全部收起", modeFull: "显示模式:完整清单", modeError: "显示模式:只看异常", loadSuccess: "✅ 已加载:", files: " 个文件", labelFiles: "切换文件:", selectPlaceholder: "--- 请选择已加载的报告 ---", btnPickDir: "📂 选择结果目录", loadComplete: "已成功加载 {n} 个报告文件。" },
|
||||||
en: { name: "English", dropHint: "📂 <b>Drag</b> your file here", clickHint: "or click to select manually", labelMatch: "Match", labelMismatch: "Mismatch", labelMissing: "Missing", labelExtra: "Extra", btnExpand: "Expand All", btnCollapse: "Collapse All", modeFull: "View: All Files", modeError: "View: Errors Only", loadSuccess: "✅ Loaded: ", files: " files" }
|
en: { name: "English", dropHint: "📂 <b>Drag</b> your file here", clickHint: "or click to select manually", labelMatch: "Match", labelMismatch: "Mismatch", labelMissing: "Missing", labelExtra: "Extra", btnExpand: "Expand All", btnCollapse: "Collapse All", modeFull: "View: All Files", modeError: "View: Errors Only", loadSuccess: "✅ Loaded: ", files: " files", labelFiles: "Switch File:", selectPlaceholder: "--- Select a loaded report ---", btnPickDir: "📂 Open Directory", loadComplete: "Successfully loaded {n} report files." }
|
||||||
};
|
};
|
||||||
|
|
||||||
// 合并外部语言
|
// 合并外部语言
|
||||||
@@ -95,8 +103,47 @@
|
|||||||
let currentLang = 'zh';
|
let currentLang = 'zh';
|
||||||
let globalData = "";
|
let globalData = "";
|
||||||
let errorMode = false;
|
let errorMode = false;
|
||||||
|
let fileHandles = {}; // 存储文件句柄
|
||||||
|
|
||||||
|
// 文件夹选择器逻辑 (File System Access API)
|
||||||
|
async function pickDirectory() {
|
||||||
|
try {
|
||||||
|
const directoryHandle = await window.showDirectoryPicker();
|
||||||
|
const dropdown = document.getElementById('fileDropdown');
|
||||||
|
dropdown.innerHTML = `<option value="" disabled selected id="t-select-placeholder">${allI18n[currentLang].selectPlaceholder}</option>`;
|
||||||
|
fileHandles = {};
|
||||||
|
|
||||||
|
for await (const entry of directoryHandle.values()) {
|
||||||
|
if (entry.kind === 'file' && entry.name.endsWith('.txt')) {
|
||||||
|
fileHandles[entry.name] = entry;
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = entry.name;
|
||||||
|
opt.innerText = entry.name;
|
||||||
|
dropdown.appendChild(opt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const msg = allI18n[currentLang].loadComplete.replace('{n}', Object.keys(fileHandles).length);
|
||||||
|
alert(msg);
|
||||||
|
} catch (err) {
|
||||||
|
if (err.name !== 'AbortError') {
|
||||||
|
console.error('选择目录失败:', err);
|
||||||
|
alert('浏览器不支持该功能或选择失败,请确保使用 Chrome/Edge 并授予权限。');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSelectedFile(fileName) {
|
||||||
|
const handle = fileHandles[fileName];
|
||||||
|
if (handle) {
|
||||||
|
const file = await handle.getFile();
|
||||||
|
const text = await file.text();
|
||||||
|
globalData = text;
|
||||||
|
processData(globalData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 初始化语言选择器
|
// 初始化语言选择器
|
||||||
|
|
||||||
const langSelect = document.getElementById('langSelect');
|
const langSelect = document.getElementById('langSelect');
|
||||||
Object.keys(allI18n).forEach(key => {
|
Object.keys(allI18n).forEach(key => {
|
||||||
const opt = document.createElement('option');
|
const opt = document.createElement('option');
|
||||||
@@ -113,18 +160,29 @@
|
|||||||
|
|
||||||
function updateUI() {
|
function updateUI() {
|
||||||
const t = allI18n[currentLang];
|
const t = allI18n[currentLang];
|
||||||
document.getElementById('t-drop-hint').innerHTML = t.dropHint;
|
const setTxt = (id, txt, isHtml) => {
|
||||||
document.getElementById('t-click-hint').innerText = t.clickHint;
|
const el = document.getElementById(id);
|
||||||
document.getElementById('t-label-match').innerText = t.labelMatch;
|
if (el) isHtml ? el.innerHTML = txt : el.innerText = txt;
|
||||||
document.getElementById('t-label-mismatch').innerText = t.labelMismatch;
|
};
|
||||||
document.getElementById('t-label-missing').innerText = t.labelMissing;
|
|
||||||
document.getElementById('t-label-extra').innerText = t.labelExtra;
|
setTxt('t-drop-hint', t.dropHint, true);
|
||||||
document.getElementById('t-btn-expand').innerText = t.btnExpand;
|
setTxt('t-click-hint', t.clickHint);
|
||||||
document.getElementById('t-btn-collapse').innerText = t.btnCollapse;
|
setTxt('t-label-match', t.labelMatch);
|
||||||
|
setTxt('t-label-mismatch', t.labelMismatch);
|
||||||
|
setTxt('t-label-missing', t.labelMissing);
|
||||||
|
setTxt('t-label-extra', t.labelExtra);
|
||||||
|
setTxt('t-btn-expand', t.btnExpand);
|
||||||
|
setTxt('t-btn-collapse', t.btnCollapse);
|
||||||
|
setTxt('t-label-files', t.labelFiles);
|
||||||
|
setTxt('t-select-placeholder', t.selectPlaceholder);
|
||||||
|
setTxt('btnSelectDir', t.btnPickDir);
|
||||||
|
|
||||||
const filterBtn = document.getElementById('toggleFilterBtn');
|
const filterBtn = document.getElementById('toggleFilterBtn');
|
||||||
|
if (filterBtn) {
|
||||||
filterBtn.innerText = errorMode ? t.modeError : t.modeFull;
|
filterBtn.innerText = errorMode ? t.modeError : t.modeFull;
|
||||||
filterBtn.className = errorMode ? 'btn btn-danger' : 'btn btn-primary';
|
filterBtn.className = errorMode ? 'btn btn-danger' : 'btn btn-primary';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Rclone 处理逻辑 ---
|
// --- Rclone 处理逻辑 ---
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
@@ -146,12 +204,21 @@
|
|||||||
|
|
||||||
function handleFile(file) {
|
function handleFile(file) {
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
// 重置下拉框到默认占位符选项
|
||||||
|
const dropdown = document.getElementById('fileDropdown');
|
||||||
|
if (dropdown) dropdown.selectedIndex = 0;
|
||||||
|
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (e) => { globalData = e.target.result; processData(globalData); };
|
reader.onload = (e) => { globalData = e.target.result; processData(globalData); };
|
||||||
reader.readAsText(file, 'UTF-8');
|
reader.readAsText(file, 'UTF-8');
|
||||||
}
|
}
|
||||||
|
|
||||||
function processData(text) {
|
function processData(text) {
|
||||||
|
if (!text) return;
|
||||||
|
// 处理 PowerShell 偶尔可能返回数组或非字符串对象的情况
|
||||||
|
if (Array.isArray(text)) text = text.join('\n');
|
||||||
|
if (typeof text !== 'string') text = String(text);
|
||||||
|
|
||||||
const lines = text.split(/\r?\n/);
|
const lines = text.split(/\r?\n/);
|
||||||
const tree = {};
|
const tree = {};
|
||||||
const stats = {'=':0, '*':0, '-':0, '+':0};
|
const stats = {'=':0, '*':0, '-':0, '+':0};
|
||||||
@@ -178,8 +245,8 @@
|
|||||||
|
|
||||||
document.getElementById('s-match').innerText = stats['='];
|
document.getElementById('s-match').innerText = stats['='];
|
||||||
document.getElementById('s-mismatch').innerText = stats['*'];
|
document.getElementById('s-mismatch').innerText = stats['*'];
|
||||||
document.getElementById('s-missing').innerText = stats['-'];
|
document.getElementById('s-missing').innerText = stats['+'];
|
||||||
document.getElementById('s-extra').innerText = stats['+'];
|
document.getElementById('s-extra').innerText = stats['-'];
|
||||||
document.getElementById('report-content').style.display = 'block';
|
document.getElementById('report-content').style.display = 'block';
|
||||||
document.getElementById('treeContainer').innerHTML = buildHtml(tree, "", folderErrors);
|
document.getElementById('treeContainer').innerHTML = buildHtml(tree, "", folderErrors);
|
||||||
const t = allI18n[currentLang];
|
const t = allI18n[currentLang];
|
||||||
@@ -202,7 +269,7 @@
|
|||||||
<details><summary>📁 ${name}/ ${hasErr?'<span class="error-dot">●</span>':''}</summary>${buildHtml(val, thisPath, folderErrors)}</details>
|
<details><summary>📁 ${name}/ ${hasErr?'<span class="error-dot">●</span>':''}</summary>${buildHtml(val, thisPath, folderErrors)}</details>
|
||||||
</li>`;
|
</li>`;
|
||||||
} else {
|
} else {
|
||||||
const colors = {'=':'#28a745', '*':'#dc3545', '-':'#ffc107', '+':'#17a2b8'};
|
const colors = {'=':'#28a745', '*':'#dc3545', '+': '#ffc107', '-': '#17a2b8'};
|
||||||
html += `<li class="file ${val==='='?'is-match':'is-error'}">
|
html += `<li class="file ${val==='='?'is-match':'is-error'}">
|
||||||
<span class="badge" style="background:${colors[val]}">${val}</span>${name}
|
<span class="badge" style="background:${colors[val]}">${val}</span>${name}
|
||||||
</li>`;
|
</li>`;
|
||||||
|
|||||||
225
Size_Comparator.html
Normal file
225
Size_Comparator.html
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Rclone 尺寸对比器</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--success: #28a745; --danger: #dc3545; --warning: #ffc107; --info: #17a2b8;
|
||||||
|
--bg: #f8f9fa; --card-bg: #ffffff; --text: #2d3436; --primary: #0984e3;
|
||||||
|
}
|
||||||
|
body { font-family: 'Segoe UI', system-ui, sans-serif; background: var(--bg); color: var(--text); padding: 20px; margin: 0; }
|
||||||
|
.container { max-width: 1100px; margin: auto; }
|
||||||
|
|
||||||
|
/* 拖拽区 */
|
||||||
|
.drop-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 25px; }
|
||||||
|
.drop-box { border: 2px dashed #bdc3c7; border-radius: 12px; padding: 5px 15px; text-align: center; background: white; cursor: pointer; transition: 0.2s; }
|
||||||
|
.drop-box.loaded { border-color: var(--success); background: #f6ffed; }
|
||||||
|
.drop-box.hover { border-color: var(--primary); background: #e6f7ff; }
|
||||||
|
|
||||||
|
/* 统计卡片 - 增加到4项 */
|
||||||
|
.stat-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 15px; margin-bottom: 25px; display: none; }
|
||||||
|
.stat-card { background: white; padding: 12px; border-radius: 10px; text-align: center; box-shadow: 0 4px 6px rgba(0,0,0,0.05); border-bottom: 4px solid #ddd; }
|
||||||
|
.stat-val { font-size: 24px; font-weight: 800; display: block; margin-bottom: 4px; }
|
||||||
|
.stat-label { font-size: 12px; color: #636e72; font-weight: 600; text-transform: uppercase; }
|
||||||
|
|
||||||
|
/* 报告区域 */
|
||||||
|
.report-card { background: white; padding: 30px; border-radius: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.08); display: none; }
|
||||||
|
.btn-group { margin-bottom: 20px; display: flex; gap: 10px; }
|
||||||
|
.btn { padding: 9px 18px; border: 1px solid #dfe6e9; border-radius: 8px; background: white; cursor: pointer; font-weight: 600; transition: 0.2s; }
|
||||||
|
.btn:hover { background: #f1f2f6; }
|
||||||
|
.btn-active { background: var(--danger) !important; color: white; border-color: var(--danger); }
|
||||||
|
|
||||||
|
/* 树状 UI */
|
||||||
|
ul { list-style: none; padding-left: 22px; border-left: 1px dashed #dfe6e9; margin: 6px 0; }
|
||||||
|
li { margin: 5px 0; }
|
||||||
|
.item-row { display: flex; align-items: center; padding: 4px 8px; border-radius: 5px; transition: 0.1s; }
|
||||||
|
.item-row:hover { background: #f8f9fa; }
|
||||||
|
|
||||||
|
summary { cursor: pointer; outline: none; list-style: none; }
|
||||||
|
summary::-webkit-details-marker { display: none; }
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
display: inline-block; min-width: 22px; height: 18px; line-height: 18px;
|
||||||
|
text-align: center; border-radius: 4px; color: white; font-size: 11px;
|
||||||
|
font-weight: bold; margin-right: 12px; flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-icon { margin-right: 8px; font-size: 1.1em; opacity: 0.7; }
|
||||||
|
.error-dot { color: var(--danger); margin-left: 8px; font-size: 14px; animation: blink 2s infinite; }
|
||||||
|
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
|
||||||
|
|
||||||
|
/* 过滤模式:隐藏一致的项目 */
|
||||||
|
.filter-mode .is-match, .filter-mode .is-pure-match { display: none; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h2 style="text-align:center; margin-bottom:30px;">🎬 Rclone 双向对比工具</h2>
|
||||||
|
|
||||||
|
<div class="drop-grid">
|
||||||
|
<div id="drop-remote" class="drop-box"><p id="t-remote">1. 拖入云端清单</p><input type="file" id="file-remote" style="display:none"></div>
|
||||||
|
<div id="drop-local" class="drop-box"><p id="t-local">2. 拖入本地清单</p><input type="file" id="file-local" style="display:none"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="report-section">
|
||||||
|
<div class="stat-grid" id="stat-bar">
|
||||||
|
<div class="stat-card" style="border-color: var(--success)"><span class="stat-label">一致 (Match)</span><span id="cnt-match" class="stat-val" style="color: var(--success)">0</span></div>
|
||||||
|
<div class="stat-card" style="border-color: var(--danger)"><span class="stat-label">不同 (Diff)</span><span id="cnt-diff" class="stat-val" style="color: var(--danger)">0</span></div>
|
||||||
|
<div class="stat-card" style="border-color: var(--warning)"><span class="stat-label">缺失 (Missing)</span><span id="cnt-miss" class="stat-val" style="color: var(--warning)">0</span></div>
|
||||||
|
<div class="stat-card" style="border-color: var(--info)"><span class="stat-label">多余 (Extra)</span><span id="cnt-extra" class="stat-val" style="color: var(--info)">0</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="report-card" id="report-card">
|
||||||
|
<div class="btn-group">
|
||||||
|
<button onclick="toggleFilter()" id="filterBtn" class="btn">只看异常</button>
|
||||||
|
<button onclick="toggleAll(true)" class="btn">全部展开</button>
|
||||||
|
<button onclick="toggleAll(false)" class="btn">全部收起</button>
|
||||||
|
</div>
|
||||||
|
<div id="treeContainer"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let remoteMap = null, localMap = null, errorMode = false;
|
||||||
|
|
||||||
|
function setupBox(id, fileId, type) {
|
||||||
|
const box = document.getElementById(id);
|
||||||
|
const input = document.getElementById(fileId);
|
||||||
|
box.onclick = () => input.click();
|
||||||
|
box.ondragover = (e) => { e.preventDefault(); box.classList.add('hover'); };
|
||||||
|
box.ondragleave = () => box.classList.remove('hover');
|
||||||
|
box.ondrop = (e) => { e.preventDefault(); box.classList.remove('hover'); handleFile(e.dataTransfer.files[0], type); };
|
||||||
|
input.onchange = (e) => handleFile(e.target.files[0], type);
|
||||||
|
}
|
||||||
|
|
||||||
|
setupBox('drop-remote', 'file-remote', 'remote');
|
||||||
|
setupBox('drop-local', 'file-local', 'local');
|
||||||
|
|
||||||
|
function handleFile(file, type) {
|
||||||
|
if (!file) return;
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const map = new Map();
|
||||||
|
e.target.result.split(/\r?\n/).forEach(line => {
|
||||||
|
if (!line.trim()) return;
|
||||||
|
const parts = line.split('|');
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
const size = parts[0].trim();
|
||||||
|
const path = parts.slice(1).join('|').replace(/^\/+|\/+$/g, '');
|
||||||
|
map.set(path, size);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (type === 'remote') { remoteMap = map; document.getElementById('t-remote').innerText = "✅ 云端清单已加载"; document.getElementById('drop-remote').classList.add('loaded'); }
|
||||||
|
else { localMap = map; document.getElementById('t-local').innerText = "✅ 本地清单已加载"; document.getElementById('drop-local').classList.add('loaded'); }
|
||||||
|
if (remoteMap && localMap) startComparison();
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startComparison() {
|
||||||
|
const tree = {}, stats = {match:0, diff:0, miss:0, extra:0}, folderErrors = new Set();
|
||||||
|
|
||||||
|
// 获取所有路径的并集
|
||||||
|
const allPaths = new Set([...remoteMap.keys(), ...localMap.keys()]);
|
||||||
|
|
||||||
|
allPaths.forEach(path => {
|
||||||
|
const rSize = remoteMap.get(path);
|
||||||
|
const lSize = localMap.get(path);
|
||||||
|
let symbol = '';
|
||||||
|
|
||||||
|
if (rSize !== undefined && lSize !== undefined) {
|
||||||
|
if (rSize === lSize) { symbol = '='; stats.match++; }
|
||||||
|
else { symbol = '*'; stats.diff++; }
|
||||||
|
} else if (rSize !== undefined && lSize === undefined) {
|
||||||
|
symbol = '-'; stats.miss++; // 缺失
|
||||||
|
} else {
|
||||||
|
symbol = '+'; stats.extra++; // 多余
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建树状结构
|
||||||
|
let curr = tree;
|
||||||
|
const parts = path.split('/');
|
||||||
|
parts.forEach((part, i) => {
|
||||||
|
if (i === parts.length - 1) {
|
||||||
|
curr[part] = symbol;
|
||||||
|
} else {
|
||||||
|
if (!curr[part] || typeof curr[part] === 'string') curr[part] = {};
|
||||||
|
curr = curr[part];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 标记含异常的文件夹
|
||||||
|
if (symbol !== '=') {
|
||||||
|
let acc = "";
|
||||||
|
parts.slice(0, -1).forEach(p => {
|
||||||
|
acc = acc ? `${acc}/${p}` : p;
|
||||||
|
folderErrors.add(acc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新统计 UI
|
||||||
|
document.getElementById('cnt-match').innerText = stats.match;
|
||||||
|
document.getElementById('cnt-diff').innerText = stats.diff;
|
||||||
|
document.getElementById('cnt-miss').innerText = stats.miss;
|
||||||
|
document.getElementById('cnt-extra').innerText = stats.extra;
|
||||||
|
|
||||||
|
document.getElementById('stat-bar').style.display = 'grid';
|
||||||
|
document.getElementById('report-card').style.display = 'block';
|
||||||
|
document.getElementById('treeContainer').innerHTML = renderTree(tree, "", folderErrors);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTree(node, currentPath, errors) {
|
||||||
|
let html = "<ul>";
|
||||||
|
const sortedKeys = Object.keys(node).sort((a, b) => {
|
||||||
|
const aIsFolder = typeof node[a] === 'object';
|
||||||
|
const bIsFolder = typeof node[b] === 'object';
|
||||||
|
return aIsFolder === bIsFolder ? a.localeCompare(b) : (aIsFolder ? -1 : 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
sortedKeys.forEach(name => {
|
||||||
|
const val = node[name];
|
||||||
|
const thisPath = currentPath ? `${currentPath}/${name}` : name;
|
||||||
|
|
||||||
|
if (typeof val === 'object') {
|
||||||
|
const hasErr = errors.has(thisPath);
|
||||||
|
html += `<li class="folder ${hasErr?'has-error':'is-pure-match'}">
|
||||||
|
<details>
|
||||||
|
<summary><div class="item-row"><span class="folder-icon">📁</span>${name}/ ${hasErr?'<span class="error-dot">●</span>':''}</div></summary>
|
||||||
|
${renderTree(val, thisPath, errors)}
|
||||||
|
</details>
|
||||||
|
</li>`;
|
||||||
|
} else {
|
||||||
|
const cfg = {
|
||||||
|
'=': {c: 'var(--success)', s: '='},
|
||||||
|
'*': {c: 'var(--danger)', s: '*'},
|
||||||
|
'-': {c: 'var(--warning)', s: '-'},
|
||||||
|
'+': {c: 'var(--info)', s: '+'}
|
||||||
|
}[val];
|
||||||
|
|
||||||
|
html += `<li class="${val==='='?'is-match':'is-error'}">
|
||||||
|
<div class="item-row">
|
||||||
|
<span class="badge" style="background:${cfg.c}">${cfg.s}</span>
|
||||||
|
<span class="file-name">${name}</span>
|
||||||
|
</div>
|
||||||
|
</li>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return html + "</ul>";
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleFilter() {
|
||||||
|
errorMode = !errorMode;
|
||||||
|
document.getElementById('treeContainer').classList.toggle('filter-mode', errorMode);
|
||||||
|
const btn = document.getElementById('filterBtn');
|
||||||
|
btn.innerText = errorMode ? "显示全部" : "只看异常";
|
||||||
|
btn.classList.toggle('btn-active', errorMode);
|
||||||
|
if(errorMode) document.querySelectorAll('.has-error > details').forEach(d => d.open = true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleAll(open) { document.querySelectorAll('details').forEach(d => d.open = open); }
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -13,7 +13,7 @@ set "COMBINED_FILE=%FILE_BASENAME%.txt"
|
|||||||
if not exist "combined_files" mkdir "combined_files"
|
if not exist "combined_files" mkdir "combined_files"
|
||||||
|
|
||||||
echo Running rclone check ...
|
echo Running rclone check ...
|
||||||
rclone check "%MD5_PATH%" "%TARGET_DIR%" --local-encoding "None" --exclude-from exclude-list.txt --checkfile md5 --combined "combined_files\%COMBINED_FILE%"
|
rclone check "%MD5_PATH%" "%TARGET_DIR%" --local-encoding "None" --exclude-from exclude-list.txt --checkfile md5 --transfers 1 --buffer-size 256M --checkers 8 --combined "combined_files\%COMBINED_FILE%"
|
||||||
|
|
||||||
if %ERRORLEVEL% equ 0 (
|
if %ERRORLEVEL% equ 0 (
|
||||||
echo Success: Saved to "combined_files\%COMBINED_FILE%"
|
echo Success: Saved to "combined_files\%COMBINED_FILE%"
|
||||||
|
|||||||
26
gen_combined.sh
Normal file
26
gen_combined.sh
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if [ -z "$1" ] || [ -z "$2" ]; then
|
||||||
|
echo "Usage: $0 <md5_file_path> <target_dir>"
|
||||||
|
echo "Example: $0 md5_files/mybackup.md5 /Users/yourname/Backup"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
MD5_PATH="$1"
|
||||||
|
TARGET_DIR="$2"
|
||||||
|
|
||||||
|
FILENAME=$(basename "$MD5_PATH")
|
||||||
|
|
||||||
|
FILE_BASENAME="${FILENAME%.*}"
|
||||||
|
COMBINED_FILE="${FILE_BASENAME}.txt"
|
||||||
|
|
||||||
|
mkdir -p "combined_files"
|
||||||
|
|
||||||
|
echo "Running ./rclone check ..."
|
||||||
|
./rclone check "$MD5_PATH" "$TARGET_DIR" --local-encoding "None" --exclude-from "exclude-list.txt" --checkfile "md5" --transfers 1 --buffer-size "256M" --checkers 8 --combined "combined_files/$COMBINED_FILE"
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo "Success: Saved to "combined_files/$COMBINED_FILE""
|
||||||
|
else
|
||||||
|
echo "Note: rclone check finished (return value might be different)"
|
||||||
|
fi
|
||||||
25
gen_sizeinfo.bat
Normal file
25
gen_sizeinfo.bat
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
|
||||||
|
if "%~1"=="" goto usage
|
||||||
|
if "%~2"=="" goto usage
|
||||||
|
|
||||||
|
set TARGET_PATH=%~1
|
||||||
|
set FILE_NAME=%~2
|
||||||
|
|
||||||
|
if not exist "man_files" mkdir "man_files"
|
||||||
|
|
||||||
|
echo Running rclone lsf ...
|
||||||
|
rclone lsf -R --format "sp" --separator "|" --local-encoding "None" --exclude-from exclude-list.txt "%TARGET_PATH%" > "man_files\%FILE_NAME%"
|
||||||
|
|
||||||
|
if %ERRORLEVEL% equ 0 (
|
||||||
|
echo Success: File saved to man_files\%FILE_NAME%
|
||||||
|
) else (
|
||||||
|
echo Error: rclone failed
|
||||||
|
)
|
||||||
|
goto :eof
|
||||||
|
|
||||||
|
:usage
|
||||||
|
echo Usage: %~nx0 ^<remote:path^|localpath^> ^<file^>
|
||||||
|
echo Example: %~nx0 [mydrive:backup^|C:/local/path] manifest.txt
|
||||||
|
exit /b 1
|
||||||
21
gen_sizeinfo.sh
Normal file
21
gen_sizeinfo.sh
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if [ -z "$1" ] || [ -z "$2" ]; then
|
||||||
|
echo "Usage: $0 <path> <file_name>"
|
||||||
|
echo "Example: $0 mydrive:backup|/path/to/dir manifest.txt"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
TARGET_PATH="$1"
|
||||||
|
FILE_NAME="$2"
|
||||||
|
|
||||||
|
mkdir -p "man_files"
|
||||||
|
|
||||||
|
echo "Running ./rclone lsf ..."
|
||||||
|
./rclone lsf -R --format "sp" --separator "|" --local-encoding "None" --exclude-from "exclude-list.txt" "$TARGET_PATH" > "man_files/$FILE_NAME"
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo "Success: Saved to "man_files/$FILE_NAME""
|
||||||
|
else
|
||||||
|
echo "Error: rclone failed"
|
||||||
|
fi
|
||||||
@@ -13,6 +13,10 @@ window.externalI18n = {
|
|||||||
"modeFull": "Vista: Todos los archivos",
|
"modeFull": "Vista: Todos los archivos",
|
||||||
"modeError": "Vista: Solo errores",
|
"modeError": "Vista: Solo errores",
|
||||||
"loadSuccess": "✅ Informe cargado: ",
|
"loadSuccess": "✅ Informe cargado: ",
|
||||||
"files": " archivos"
|
"files": " archivos",
|
||||||
|
"labelFiles": "Cambiar archivo:",
|
||||||
|
"selectPlaceholder": "--- Seleccione un informe ---",
|
||||||
|
"btnPickDir": "📂 Directorio de resultados",
|
||||||
|
"loadComplete": "Se han cargado {n} informes con éxito."
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user