记一次近6万多个文件的备份过程

背景和条件

近期接到这样一个需求,要从用户的服务器上备份将近6万多个文件,这些文件有大有小,小的几百Kb,大的几GB,要尽可能快的完成这些文件的备份工作。

我大概总结了一下,限制条件基本是以下几点

  1. 文件多,有大有小,总量约500多G
  2. 文件是分散存储的,也就是不能简单的去做打包操作,然后在下载备份;
  3. 也不能在用户的服务器做整理性操作(比如先拷贝到一处),因为用户服务器还没有完成扩容,剩余的存储空间只有100G,操作空间不够;
  4. 这些文件有些是损坏的,因为用户上传的初始文件就是损坏的,备份的时候,要能分辨出那些是正常的,那些是损坏的。
  5. 备份后的文件其存储目录和原本的存储目录不同,比如原本文件的存储是"年份\大类\编号\文件",备份的策略则是"年份\大类\组别\编号+项目名称\文件",不仅多了一级,而且子级里的命名规则也改了!
  6. 尽快。

基本就是这些限制条件了。

事实上,第5个限制条件,一般应该不常见!毕竟备份一般都是原样备份,而这种改存储规则的需求有点自废武功的感觉,你要怎么定位这些备份文件呢?肯定会增加额外的工作量。

操作思路

最开始我想到的思路,就是写一个备份脚本,在用户服务器上先把需要备份的文件整理到一处,然后打包备份就完事儿了。

但由于剩余空间不支持这样做,且暂时无法扩容,更不能删除其他文件去释放空间,所以好像也只有下载到本地进行整理这一个方向了。

⚠️:注意,在自己的电脑上,把文件粘来粘去是ok的,但在生产服务器或者别人的服务器上,尽量不要这样操作,即便只是简单的复制也要避免,一旦要操作的文件数量大,很容易出错!
⚠️:就算你要写程序操作,然后在用户机器上执行,也要十分的谨慎小心,毕竟不是自己的财产,要考虑你的程序是否有内存溢出等结构性风险,会不会占用过多资源,你的程序是否有频繁的I/O操作,如果有,是否是阻塞性操作,会不会造成死机风险,出现死机了怎么办等等。

操作步骤

整理目录文件

这里的整理操作,是先通过技术手段,如脚本,查询数据库等,把要备份的文件整理到Excel或者其他可读性较好的文件中,我这里是整理到了Excel里,像👇这样。

分割目录文件

因为要备份的文件有近6万条记录,所以再单个终端上下载肯定是不合适。所以我做了分割,每5000条一个文件

准备下载脚本

这里我是使用的PowerShell脚本来执行下载操作,脚本代码如下

powershell 复制代码
param (
    [string]$specificExcelFile = $null
)

# 安装并导入ImportExcel模块
if (-not (Get-Module -ListAvailable -Name ImportExcel)) {
    Install-Module -Name ImportExcel -Scope CurrentUser -Force
}
Import-Module ImportExcel

# 定义输入参数
$excelDir = "F:\备份附件"
$successLogFile = "$($excelDir)\success_log.txt"
$failureLogFile = "$($excelDir)\failure_log.txt"

# 创建日志文件(如果不存在则创建)
if (-Not (Test-Path $successLogFile)) { New-Item -ItemType File -Path $successLogFile -Force }
if (-Not (Test-Path $failureLogFile)) { New-Item -ItemType File -Path $failureLogFile -Force }

function Log-Message {
    param (
        [string]$logFile,
        [string]$message
    )
    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $logEntry = "$timestamp - $message"
    Add-Content -Path $logFile -Value $logEntry
}

function Download-WithResume {
    param (
        [string]$url,
        [string]$destination,
        [string]$year,
        [string]$competition,
        [string]$group,
        [string]$projectNo,
        [string]$projectName,
        [string]$attachmentName
    )
    try {
        # 构建保存路径,注意这里在项目编号和项目名称之间添加了下划线
		$savePath = Join-Path -Path "$excelDir\$year\$competition\$group\$projectNo`_$projectName" -ChildPath $attachmentName

        # 检查文件是否存在,如果存在则跳过
        if (Test-Path $savePath) {
            Write-Host "文件已存在,跳过: $savePath"
            return
        }

        # 创建目录
        $dirPath = Split-Path -Parent $savePath
        if (-Not (Test-Path -Path $dirPath)) {
            New-Item -ItemType Directory -Path $dirPath -Force
        }

        # 下载文件(支持断点续传)
        if (Test-Path $savePath) {
            $existingLength = (Get-Item $savePath).length
            $headers = @{ "Range" = "bytes=$existingLength-" }
            Invoke-WebRequest -Uri $url -OutFile $savePath -Headers $headers -Method Get
        } else {
            Invoke-WebRequest -Uri $url -OutFile $savePath
        }
        Log-Message -logFile $successLogFile -message "下载成功: $url ,已保存到" $savePath ""
        Write-Host "Downloading file: $savePath"
    } catch {
        $errorMessage = "下载失败: $url. 异常信息: $_"
        Log-Message -logFile $failureLogFile -message $errorMessage
        Write-Host $errorMessage
    }
}

if ($specificExcelFile) {
    # 如果指定了具体的Excel文件,则直接处理这个文件
    if (Test-Path $specificExcelFile) {
        $excelFilePath = $specificExcelFile
        Write-Host "正在读取: $excelFilePath"

        # 读取Excel文件
        $excelData = Import-Excel -Path $excelFilePath

        # 遍历每一行数据
        foreach ($row in $excelData) {
            $year = $row.year
            $competition = $row.competition
            $group = $row.group
            $projectNo = $row.projectNo
            $projectName = $row.projectName
            $attachmentName = $row.attachmentName
            $downloadUrl = $row.downloadUrl

            # 调用下载函数
            Download-WithResume -url $downloadUrl -destination $savePath -year $year -competition $competition -group $group -projectNo $projectNo -projectName $projectName -attachmentName $attachmentName
        }
    } else {
        Write-Host "excel文件不存在: $specificExcelFile"
    }
} else {
    # 否则遍历目录下的所有Excel文件
    Get-ChildItem -Path $excelDir -Filter *.xlsx | ForEach-Object {
        $excelFilePath = $_.FullName
        Write-Host "读取文件: $excelFilePath"

        # 读取Excel文件
        $excelData = Import-Excel -Path $excelFilePath

        # 遍历每一行数据
        foreach ($row in $excelData) {
            $year = $row.year
            $competition = $row.competition
            $group = $row.group
            $projectNo = $row.projectNo
            $projectName = $row.projectName
            $attachmentName = $row.attachmentName
            $downloadUrl = $row.downloadUrl

            # 调用下载函数
            Download-WithResume -url $downloadUrl -destination $savePath -year $year -competition $competition -group $group -projectNo $projectNo -projectName $projectName -attachmentName $attachmentName
        }
    }
}

这段脚本,就是读取当前目录下的所有excel文件,然后依次下载,其中涉及到了环境检测,比如安装ImportExcel,如果本地的PowerShell环境没有安装,则会进行安装,然后执行业务逻辑。

🌟:如果大家运维的服务器或者开发环境中,windows环境比较多,那掌握点PowerShell知识还是很方便的,对于一些简单但重复性较高的工作,它比Python之类的脚本还要好用的多,至少Python你还要单独安装Python环境,还要用pip安装额外的依赖包,还要注意区分版本,这些你在开发环境搞搞没问题,而生产环境一般是不能随意安装额外的环境的.....而PowerShell本身就集成在了操作系统里,它的代码也高度抽象,通常一个命令能涵盖很多功能点,十分方便。就算不会写代码也没关系,可以自己去GPT,只要能提供思路就好。

分摊下载任务

这一步,就看情况而定了,因为我是使用的脚本下载,没有多线程下载的方案,就多找了几台终端,同时启动下载,也算是真·多线程(多终端)了吧。

下载任务启动以后,剩下的就是等着下载完成了,当然了,如果是多终端下载,还要做最终的合并整理,这个耗时也不会短,毕竟几百个G的总量在那摆着。

验证下载 *

下载完成以后,就是验证下载的文件是否都正常,比如图片,pdf,word等文档是否能正常打开,视频是否能正常播放,压缩包是否可以正常解压。

这一步实际上属于补偿措施,因为用户那里上传的文件,是存在这种没有完整上传的记录,而且也没有做完整性校验,所以要做这个续补偿操作。

其实,这一步没办法做到100%验证,只能说尽可能的尝试一下,提供一个参考。

这个,就看实际情况了,怎么实现都行,只要能验证文件是否有效,可以输出一个可读性较好的记录就好。

我这里就列一下PDF和图片文件的验证,代码是Python实现的,这个工作,好像没有其他语言比Python更合适,因为可选的插件真的太多了!

更多的验证,大家自行实现或者GPT即可。

python 复制代码
# 验证图片是否能打开
def is_image_readable(file_path):
    try:
        with Image.open(file_path) as img:
            img.verify()
        return True
    except Exception as e:
        fail_logger.error(f"图像文件无效或已损坏: {file_path} - 错误: {e}")
        return False
        
# 安装PyPDF2
# pip install PyPDF2
# 验证pdf文件是否能打开
from PyPDF2 import PdfReader

def is_pdf_readable(file_path):
    try:
        with open(file_path, 'rb') as f:
            reader = PdfReader(f)
            # 检查文件是否至少包含一页
            if len(reader.pages) > 0:
                return True
            else:
                fail_logger.error(f"PDF文件无效或为空: {file_path}")
                return False
    except Exception as e:
        fail_logger.error(f"PDF文件无效或已损坏: {file_path} - 错误: {e}")
        return False

总结

其实整个流程捋下来,没什么硬核的技术,主要还是思路,当然我这只是万千可实现需求的一个分支方向,我是真的喜欢基于脚本的解决方案,即用即走,高效快捷。

事实上,尽管当下现代应用的开发已经十分普及,文件的存储方案,相当一部分都采用了基于AWS S3协议的存储形式,比如存到自建的MinIO集群,或者存到阿里云的OSS等,然后在搭配CDN等周边服务提供统一高效的静态资源服务,但不可否认的是,仍然有相当一部分的应用仍旧在采用传统的存储方式,比如就存在本机的指定目录,或者使用SMB协议,把文件存到一个固定的附件服务器等的,所以我们在设计文件上传和存储模块的时候,也要考虑到多种的存储形式,然后结合实际的场景可以方便的进行切换。

好了,就这些啦,下次见可爱的攻城狮萌~

相关推荐
程序视点4 分钟前
Window 10文件拷贝总是卡很久?快来试试这款小工具,榨干硬盘速度!
windows
wuk99834 分钟前
基于MATLAB编制的锂离子电池伪二维模型
linux·windows·github
优创学社21 小时前
基于springboot的社区生鲜团购系统
java·spring boot·后端
why技术1 小时前
Stack Overflow,轰然倒下!
前端·人工智能·后端
幽络源小助理1 小时前
SpringBoot基于Mysql的商业辅助决策系统设计与实现
java·vue.js·spring boot·后端·mysql·spring
lzb_kkk2 小时前
【C++】C++四种类型转换操作符详解
开发语言·c++·windows·1024程序员节
ai小鬼头2 小时前
AIStarter如何助力用户与创作者?Stable Diffusion一键管理教程!
后端·架构·github
简佐义的博客2 小时前
破解非模式物种GO/KEGG注释难题
开发语言·数据库·后端·oracle·golang
Code blocks3 小时前
使用Jenkins完成springboot项目快速更新
java·运维·spring boot·后端·jenkins