mirror of
https://github.com/chaos-zhu/easynode.git
synced 2026-09-03 06:24:12 +08:00
199 lines
5.3 KiB
JavaScript
199 lines
5.3 KiB
JavaScript
const fs = require('fs')
|
|
const path = require('path')
|
|
const { spawnSync } = require('child_process')
|
|
|
|
const projectRoot = path.resolve(__dirname, '..')
|
|
const projectName = path.basename(projectRoot)
|
|
const projectParent = path.dirname(projectRoot)
|
|
const backupDir = projectParent
|
|
|
|
// Only downloaded dependencies, generated build content and caches are
|
|
// excluded. This intentionally does not read .gitignore, so the complete .git
|
|
// repository, local scripts, ignored Plus source, databases, environment files
|
|
// and signing files are all included in the backup.
|
|
const excludedDirectoryNames = new Set([
|
|
'.agents',
|
|
'.cache',
|
|
'.cxx',
|
|
'.dart_tool',
|
|
'.gradle',
|
|
'.hvigor',
|
|
'.idea',
|
|
'.next',
|
|
'.nuxt',
|
|
'.output',
|
|
'.parcel-cache',
|
|
'.pub',
|
|
'.pub-cache',
|
|
'.swiftpm',
|
|
'.vite',
|
|
'Pods',
|
|
'DerivedData',
|
|
'backups',
|
|
'build',
|
|
'coverage',
|
|
'dist',
|
|
'dist-ssr',
|
|
'node_modules',
|
|
'oh_modules'
|
|
])
|
|
|
|
const excludedDirectoryPaths = new Set([
|
|
'native/ios/.symlinks',
|
|
'native/linux/flutter/ephemeral',
|
|
'native/macos/Flutter/ephemeral',
|
|
'native/ohos/entry/libs',
|
|
'native/ohos/entry/src/main/resources/rawfile/flutter_assets',
|
|
'native/windows/flutter/ephemeral',
|
|
'server/app/socket/sftp-cache',
|
|
'server/app/static'
|
|
])
|
|
|
|
const excludedFilePaths = new Set([
|
|
'easynode-server.zip',
|
|
'native/.flutter-plugins-dependencies',
|
|
'native/android/gradle/wrapper/gradle-wrapper.jar',
|
|
'native/ios/Flutter/Flutter.podspec',
|
|
'native/ios/Flutter/Generated.xcconfig',
|
|
'native/ios/Flutter/flutter_export_environment.sh'
|
|
])
|
|
|
|
function toArchivePath(relativePath) {
|
|
return `${projectName}/${relativePath.split(path.sep).join('/')}`
|
|
}
|
|
|
|
function isExcludedDirectory(relativePath, name) {
|
|
const normalizedPath = relativePath.split(path.sep).join('/')
|
|
return excludedDirectoryNames.has(name) || excludedDirectoryPaths.has(normalizedPath)
|
|
}
|
|
|
|
function isExcludedFile(relativePath, name) {
|
|
const normalizedPath = relativePath.split(path.sep).join('/')
|
|
|
|
if (excludedFilePaths.has(normalizedPath)) return true
|
|
if (name === '.DS_Store' || name.endsWith('.iml')) return true
|
|
if (/^GeneratedPluginRegistrant\./.test(name)) return true
|
|
|
|
return false
|
|
}
|
|
|
|
function collectFiles(directory, relativeDirectory = '') {
|
|
const files = []
|
|
|
|
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
const relativePath = path.join(relativeDirectory, entry.name)
|
|
const absolutePath = path.join(directory, entry.name)
|
|
|
|
if (entry.isDirectory()) {
|
|
if (!isExcludedDirectory(relativePath, entry.name)) {
|
|
files.push(...collectFiles(absolutePath, relativePath))
|
|
}
|
|
continue
|
|
}
|
|
|
|
if (!isExcludedFile(relativePath, entry.name)) {
|
|
files.push(toArchivePath(relativePath))
|
|
}
|
|
}
|
|
|
|
return files
|
|
}
|
|
|
|
function collectTrackedFiles() {
|
|
const result = spawnSync('git', ['ls-files', '-z'], {
|
|
cwd: projectRoot,
|
|
encoding: 'utf8',
|
|
maxBuffer: 10 * 1024 * 1024
|
|
})
|
|
|
|
if (result.error) throw result.error
|
|
if (result.status !== 0) {
|
|
throw new Error(result.stderr.trim() || '无法读取 Git 已跟踪文件列表')
|
|
}
|
|
|
|
return result.stdout
|
|
.split('\0')
|
|
.filter(Boolean)
|
|
.filter((relativePath) => {
|
|
try {
|
|
fs.lstatSync(path.join(projectRoot, relativePath))
|
|
return true
|
|
} catch (error) {
|
|
if (error.code === 'ENOENT') return false
|
|
throw error
|
|
}
|
|
})
|
|
.map(toArchivePath)
|
|
}
|
|
|
|
function formatTimestamp(date) {
|
|
const part = (value) => String(value).padStart(2, '0')
|
|
return [
|
|
date.getFullYear(),
|
|
part(date.getMonth() + 1),
|
|
part(date.getDate()),
|
|
'_',
|
|
part(date.getHours()),
|
|
part(date.getMinutes()),
|
|
part(date.getSeconds())
|
|
].join('')
|
|
}
|
|
|
|
function main() {
|
|
// A tracked file always wins over an exclusion rule. This keeps the extracted
|
|
// worktree complete even when a framework tracks a generated-looking file.
|
|
const files = [...new Set([
|
|
...collectFiles(projectRoot),
|
|
...collectTrackedFiles()
|
|
])].sort()
|
|
|
|
if (files.length === 0) {
|
|
throw new Error('没有找到需要备份的文件')
|
|
}
|
|
|
|
const invalidPath = files.find((file) => file.includes('\n'))
|
|
if (invalidPath) {
|
|
throw new Error(`文件名包含换行符,无法安全打包:${invalidPath}`)
|
|
}
|
|
|
|
fs.mkdirSync(backupDir, { recursive: true })
|
|
|
|
const timestamp = formatTimestamp(new Date())
|
|
let archivePath = path.join(backupDir, `easynode_${timestamp}.zip`)
|
|
let suffix = 2
|
|
|
|
while (fs.existsSync(archivePath)) {
|
|
archivePath = path.join(backupDir, `easynode_${timestamp}_${suffix}.zip`)
|
|
suffix += 1
|
|
}
|
|
|
|
const result = spawnSync('zip', ['-q', '-9', '-y', archivePath, '-@'], {
|
|
cwd: projectParent,
|
|
encoding: 'utf8',
|
|
input: `${files.join('\n')}\n`,
|
|
maxBuffer: 10 * 1024 * 1024
|
|
})
|
|
|
|
if (result.error && result.error.code === 'ENOENT') {
|
|
throw new Error('未找到 zip 命令,请先安装 zip')
|
|
}
|
|
|
|
if (result.error) throw result.error
|
|
if (result.status !== 0) {
|
|
throw new Error(result.stderr.trim() || `zip 执行失败,退出码:${result.status}`)
|
|
}
|
|
|
|
const size = fs.statSync(archivePath).size
|
|
const sizeInMb = (size / 1024 / 1024).toFixed(2)
|
|
|
|
console.log(`备份完成:${archivePath}`)
|
|
console.log(`文件数量:${files.length},压缩包大小:${sizeInMb} MB`)
|
|
}
|
|
|
|
try {
|
|
main()
|
|
} catch (error) {
|
|
console.error(`备份失败:${error.message}`)
|
|
process.exitCode = 1
|
|
}
|