筛选、备份及恢复本地电脑上的涉密CAD

行业特点决定了我们经常要处理涉密CAD,比如大范围的地形图。根据《保守国家秘密法》第二十九条、《计算机信息系统国际联网保密管理规定》第六条、《测绘成果管理条例》第四条等,存有保密文件的设备不得接入公共网络,同时还需定期接受相关部门的检查。

那么我们就需要经常自查,或至少在相关部门检查前开展自查。这项工作并不容易:因为可能涉密的CAD文件分散在整个硬盘内,人工检索费时费力。同时,如果将它们临时转移,每个文件又分属不同的目录,事后恢复非常困难。简单粗暴的方式是将整个工作文件夹均进行转移,但工作文件夹通常非常巨大,转移费时、费空间,同时还会影响正常业务的开展。

那么最好的办法是将“可能涉密的文件”筛选出来进行备份,同时保留其文件夹结构信息。一个powershell脚本可以做到这一点。

总体思路与关键逻辑

基本思路:

通过文件名匹配关键词和文件类型,找到对应的文件,并备份到指定文件夹内,同时保持目录树结构。例如,备份文件夹为E:\bak,要备份的文件是C:\dir\file.dwg,则备份后的文件应位于:

E:\bak\C\dir\file.dwg

基本设置:

1、用户应能指定文件名关键词,如“地形图”、“平面图”、“2000”等。

2、用户应能指定最小CAD文件大小,以命中所有大于某个体积的CAD文件。

3、用户应能指定备份文件夹。

4、用户的参数设定应友好,不应让用户去动脚本本身。

关键安全逻辑:

1、应校验备份文件夹是否存在,防止用户互相copy脚本直接执行,造成无法备份。

2、脚本应跳过备份文件夹本身,否则会陷入无限循环。

3、脚本应跳过用户映射的网络驱动器,只检索真正的本地分区。

4、需要考虑到潜在的文件权限问题,仅当备份成功时,才删除原始文件。

5、应输出完整的log日志文件。

操作友好性:

1、尽量避免用户进行命令行操作。

工具实现

设置文件:setting.txt

# 前5行是注释
# 3个参数设置如下示例:
# 备份路径="E:\My Backups"          ← 如果路径带空格则加上英文双引号
# 关键词="地形", "dxt", "1万", "2000"       ← 请用英文双引号、英文逗号分隔
# 最小文件大小(兆)=20
备份路径=
关键词="地形", "dxt", "1万", "2000", "平面", "总体", "1000", "用地", "缩图", "布置", "平纵"
最小文件大小(兆)=20

备份及删除:bak-del-dwg.ps1

function Get-Settings {
    param(
        [string]$File = (Join-Path $PSScriptRoot 'setting.txt')
    )
    if (-not (Test-Path $File)) {
        throw "缺少配置文件:$File"
    }

    $cfg = @{}
    # -Raw 能保留首行可能的 UTF-8 BOM,让后面一起 TrimStart
    $lines = (Get-Content -Path $File -Raw).Split("`n")

    foreach ($line in $lines) {
        $L = $line.Trim()                          # 去首尾空白
        $L = $L.TrimStart([char]0xFEFF)            # 去掉 UTF-8/UTF-16 BOM
        if ($L -eq '' -or $L -like '#*') { continue }

        # 用全角=也能匹配
        if ($L -match '^(.*?)\s*[==]\s*(.*)$') {
            $key = $Matches[1].Trim()
            $val = $Matches[2].Trim()

            # 去首尾英文双引号
            if ($val.StartsWith('"') -and $val.EndsWith('"')) {
                $val = $val.Substring(1, $val.Length-2)
            }
            $val = $val.TrimEnd('\','/')

            $cfg[$key] = $val
        }
    }
    return $cfg
}

# ── 读取配置 ───────────────────────────────────────────
try {
    $cfg = Get-Settings
    $BackupRoot = $cfg['备份路径']
    if (-not $BackupRoot) { throw "setting.txt 里 备份路径= 未设置" }

    # 关键词行去掉引号再分割
    $rawKW     = $cfg['关键词']
    if (-not $rawKW) { throw "setting.txt 里 关键词= 未设置" }
    $Keywords  = ($rawKW -replace '"','').Split(',') | ForEach-Object { $_.Trim() }

    $MinSizeMB = [int]($cfg['最小文件大小(兆)']  -as [int])
    $MinSizeBytes = $MinSizeMB * 1MB
} catch {
    Write-Host $_.Exception.Message -ForegroundColor Red
    exit 1
}

# ── 盘符与备份目录检查
try {
    # 1) 备份盘符存在吗?
    $driveRoot = ([IO.Path]::GetPathRoot($BackupRoot))
    if (-not (Test-Path $driveRoot)) {
        throw "备份盘符 $driveRoot 不存在,请检查setting.txt中的参数设置!"
    }

    # 2) 备份目录存在吗?不存在就尝试创建
    if (-not (Test-Path $BackupRoot)) {
        try {
            New-Item -Path $BackupRoot -ItemType Directory -Force -ErrorAction Stop | Out-Null
        } catch {
            throw "无法创建备份目录:$BackupRoot"
        }
    }
}
catch {
    Write-Host $_.Exception.Message -ForegroundColor Red
    exit 1
}

# 新增日志文件
$LogFile = Join-Path $BackupRoot ("backup_{0:yyyyMMdd_HHmmss}.log" -f (Get-Date))

function Write-Log {
    param(
        [string]$Message,
        [string]$Level = "INFO"
    )
    $timestamp = (Get-Date -Format "yyyy-MM-dd HH:mm:ss")
    $line      = "[{0}] [{1}] {2}" -f $timestamp, $Level, $Message
    # 同步输出到控制台和日志文件
    Write-Host $line
    Add-Content -Path $LogFile -Value $line -ErrorAction SilentlyContinue
}

Write-Log "=== 开始备份 (MinSizeMB=$MinSizeMB) ==="

# 获取所有本地分区
$drives = Get-PSDrive -PSProvider FileSystem | Where-Object {
    $_.DisplayRoot -notlike '\\*'          # 过滤掉映射网络盘
}

foreach ($drive in $drives) {
    Write-Host "正在扫描 $($drive.Root)..."

    Get-ChildItem -Path $drive.Root -Recurse -Filter *.dwg -ErrorAction SilentlyContinue |
    Where-Object { $_.FullName -notlike "$BackupRoot*" } |
    ForEach-Object {
        $file = $_
        $filename = $file.Name

        if (
            ($keywords | Where-Object { $filename -like "*$_*" }) -or
            ($file.Length -gt $minSizeBytes)
          ) {
            # 构建路径,保留原始分区字母作为第一层
            $relativePath = $file.FullName -replace "^([A-Z]):", '$1'
            $destPath = Join-Path $BackupRoot $relativePath

            # 确保目录存在
            $destDir = Split-Path $destPath
            if (-not (Test-Path $destDir)) {
                New-Item -Path $destDir -ItemType Directory -Force | Out-Null
            }
			
            try {
                Copy-Item -Path $file.FullName -Destination $destPath -Force -ErrorAction Stop
                Write-Log "已备份: $($file.FullName) "
            } catch {
                Write-Log "备份失败: $($file.FullName) —— $_" "ERROR"
                continue    # 跳过当前文件
            }
             # 删除源文件
            try {
                Remove-Item -Path $file.FullName -Force -ErrorAction Stop
                Write-Log "已删除源文件: $($file.FullName)"
            } catch {
                Write-Log "删除失败: $($file.FullName) —— $_" "ERROR"
            }
            Write-Host "已备份并删除: $($file.FullName)"
        }
    }
}

Write-Log "=== 备份完成 ==="

恢复:restore-dwg.ps1

function Get-Settings {
    param(
        [string]$File = (Join-Path $PSScriptRoot 'setting.txt')
    )
    if (-not (Test-Path $File)) {
        throw "缺少配置文件:$File"
    }

    $cfg = @{}
    # -Raw 能保留首行可能的 UTF-8 BOM,让后面一起 TrimStart
    $lines = (Get-Content -Path $File -Raw).Split("`n")

    foreach ($line in $lines) {
        $L = $line.Trim()                          # 去首尾空白
        $L = $L.TrimStart([char]0xFEFF)            # 去掉 UTF-8/UTF-16 BOM
        if ($L -eq '' -or $L -like '#*') { continue }

        # 用全角=也能匹配
        if ($L -match '^(.*?)\s*[==]\s*(.*)$') {
            $key = $Matches[1].Trim()
            $val = $Matches[2].Trim()

            # 去首尾英文双引号
            if ($val.StartsWith('"') -and $val.EndsWith('"')) {
                $val = $val.Substring(1, $val.Length-2)
            }
            $val = $val.TrimEnd('\','/')
            $cfg[$key] = $val
        }
    }
    return $cfg
}

# ── 读取配置 ───────────────────────────────────────────
try {
    $cfg = Get-Settings

    $BackupRoot = $cfg['备份路径']
    if (-not $BackupRoot) {
        throw 'setting.txt 里 备份路径= 未设置'
    }

    # 仅用于“还原”脚本:备份目录必须真实存在
    if (-not (Test-Path $BackupRoot)) {
        throw "备份路径 $BackupRoot 不存在,无法还原,请检查setting.txt中的参数设置!"
    }
}
catch {
    Write-Host $_.Exception.Message -ForegroundColor Red
    exit 1
}

# ── 日志文件 ───────────────────────────────────────────
$LogFile = Join-Path $BackupRoot ("restore_{0:yyyyMMdd_HHmmss}.log" -f (Get-Date))

function Write-Log {
    param(
        [string]$Message,
        [string]$Level = "INFO"
    )
    $ts   = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
    $line = "[${ts}] [$Level] $Message"
    Write-Host $line
    Add-Content -Path $LogFile -Value $line -ErrorAction SilentlyContinue
}

Write-Log "=== 开始还原 ==="

# 获取所有 .dwg 文件
Get-ChildItem -Path $BackupRoot -Recurse -Filter *.dwg | ForEach-Object {
    $backupFile = $_
    $relativePath = $backupFile.FullName.Substring($BackupRoot.Length).TrimStart('\')  # D\xxx\xxx\file.dwg

    # 防御性判断:确保第一部分是 A~Z 盘符字母
    if ($relativePath -match '^[A-Z]\\') {
        # 将路径还原成原始路径 D:\xxx\xxx\file.dwg
        $driveLetter = $relativePath.Substring(0,1)
        $restOfPath = $relativePath.Substring(1)  # 去掉 D
        $originalPath = "${driveLetter}:$restOfPath"

        # 再保险一点:避免还原到 备份目录 自己
        if ($originalPath -like "$BackupRoot*") {
            Write-Log "跳过自身文件防止死循环: $originalPath"
            continue
        }
        try {
            # 确保目录存在
            $originalDir = Split-Path $originalPath
            if (-not (Test-Path $originalDir)) {
                New-Item -Path $originalDir -ItemType Directory -Force | Out-Null
            }

            # 复制(或移动)文件回原处
            Copy-Item -Path $backupFile.FullName -Destination $originalPath -Force -ErrorAction Stop
            Write-Log "已还原: $originalPath"
        } catch {
            Write-Log "还原失败: $originalPath —— $_" "ERROR"
            continue        # 只跳过当前文件
        }
    }
    else {
        Write-Log "非法备份路径,跳过: $relativePath" "WARN"
    }
}

Write-Log "=== 全部还原完成 ==="

bat启动备份脚本:1-带路径备份并删除原文件.bat

@echo off
setlocal enabledelayedexpansion

echo.
echo 即将开始备份,请确认已正确配置 setting.txt
pause

rem 当前目录下的 ps1
set "script_dir=%~dp0"
set "ps1_path=%script_dir%bak-del-dwg.ps1"

rem 以管理员启动 PowerShell ;-NoExit 保持窗口, -Wait 让 bat 等待
powershell -NoProfile -ExecutionPolicy Bypass -Command ^
   "Start-Process powershell -Verb RunAs -Wait -ArgumentList '-NoProfile','-ExecutionPolicy','Bypass','-NoExit','-File','\"%ps1_path%\"'"

pause
endlocal

bat启动恢复脚本:2-恢复备份文件至原路径.bat

@echo off
setlocal enabledelayedexpansion

echo.
echo 即将开始恢复,请确认已正确配置 setting.txt
pause

rem 当前目录下的 ps1
set "script_dir=%~dp0"
set "ps1_path=%script_dir%restore-dwg.ps1"

rem 以管理员启动 PowerShell ;-NoExit 保持窗口, -Wait 让 bat 等待
powershell -NoProfile -ExecutionPolicy Bypass -Command ^
   "Start-Process powershell -Verb RunAs -Wait -ArgumentList '-NoProfile','-ExecutionPolicy','Bypass','-NoExit','-File','\"%ps1_path%\"'"

pause
endlocal

使用方法

文件准备

将上述5个文件放在同一个文件夹内。

1-带路径备份并删除原文件.bat
2-恢复备份文件至原路径.bat
bak-del-dwg.ps1
restore-dwg.ps1
setting.txt

环境设置

首先,如果用户没有用powershell执行过任何脚本(很可能),那么需要先做一个环境设定。

以管理员身份打开powershell,并执行:

Set-ExecutionPolicy RemoteSigned -Scope CurrentUser

当弹出yes or no时,输入y,回车。这将允许powershell执行本地脚本。这个操作只需要执行1次。

参数设置

妥善编辑setting.txt。

工具使用

接下来,双击“1-带路径备份并删除原文件.bat”,即可以管理员身份执行备份脚本;双击“2-恢复备份文件至原路径.bat”,即可以管理员身份执行恢复脚本。

恢复脚本不会删除备份文件夹中的文件。过程中生成的log文件也位于备份文件夹内。

相关工具下载:点击下载