aliyunpan/internal/command/backup.go
2021-10-10 10:48:53 +08:00

304 lines
9.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright (c) 2020 tickstep
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package command
import (
"fmt"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/aliyunpan/internal/functions/panupload"
"github.com/tickstep/library-go/logger"
"github.com/urfave/cli"
"os"
"path"
"path/filepath"
"strings"
"sync"
)
func CmdBackup() cli.Command {
return cli.Command{
Name: "backup",
Description: `备份指定 <文件/目录> 到云盘 <目标目录> 中
和上传的功能一样,只是备份多进行了如下操作
1. 增加了数据库,记录已经上传的文件信息。
目前只记录 文件位置、大小、修改时间、MD5 。
2. 上传前先根据数据库记录判断是否需要重新上传。
3. 强制同名覆盖。
注:只备份(上传)新的文件(同名覆盖),不处理删除操作。
示例:
1. 将本地的 C:\Users\Administrator\Video 整个目录备份到网盘 /视频 目录
注意区别反斜杠 "\" 和 斜杠 "/" !!!
aliyunpan-go backup C:/Users/Administrator/Video /视频
2. 将本地的 C:\Users\Administrator\Video 整个目录备份到网盘 /视频 目录,但是排除所有的.jpg文件
aliyunpan-go backup -exn "\.jpg$" C:/Users/Administrator/Video /视频
3. 将本地的 C:\Users\Administrator\Video 整个目录备份到网盘 /视频 目录,但是排除所有的.jpg文件和.mp3文件每一个排除项就是一个exn参数
aliyunpan-go backup -exn "\.jpg$" -exn "\.mp3$" C:/Users/Administrator/Video /视频
4. 将本地的 C:\Users\Administrator\Video 整个目录备份到网盘 /视频 目录,但是排除所有的 @eadir 文件夹
aliyunpan-go backup -exn "^@eadir$" C:/Users/Administrator/Video /视频
参考:
以下是典型的排除特定文件或者文件夹的例子,注意:参数值必须是正则表达式。在正则表达式中,^表示匹配开头,$表示匹配结尾。
1)排除@eadir文件或者文件夹-exn "^@eadir$"
2)排除.jpg文件-exn "\.jpg$"
3)排除.号开头的文件:-exn "^\."
4)排除~号开头的文件:-exn "^~"
5)排除 myfile.txt 文件:-exn "^myfile.txt$"
`,
Usage: "备份文件或目录",
UsageText: "backup <文件/目录路径1> <文件/目录2> <文件/目录3> ... <目标目录>",
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
Action: Backup,
Flags: append(UploadFlags, cli.BoolFlag{
Name: "delete",
Usage: "通过本地数据库记录同步删除网盘文件",
}, cli.BoolFlag{
Name: "sync",
Usage: "本地同步到网盘(会同步删除网盘文件)",
}),
}
}
func OpenSyncDb(path string) (panupload.SyncDb, error) {
return panupload.OpenSyncDb(path, BackupMetaBucketName)
}
// 删除那些本地不存在而网盘存在的网盘文件 默认使用本地数据库判断,如果 flagSync 为 true 则遍历网盘文件列表进行判断(速度较慢)。
func DelRemoteFileFromDB(driveId string, localDir string, savePath string, flagSync bool) {
activeUser := config.Config.ActiveUser()
var db panupload.SyncDb
var err error
dbpath := filepath.Join(localDir, BackupMetaDirName)
db, err = OpenSyncDb(dbpath + string(os.PathSeparator) + "db")
if err != nil {
fmt.Println("同步数据库打开失败!", err)
return
}
defer db.Close()
savePath = path.Join(savePath, filepath.Base(localDir))
//判断本地文件是否存在,如果存在返回 true 否则删除数据库相关记录和网盘上的文件。
isLocalFileExist := func(ent *panupload.UploadedFileMeta) (isExists bool) {
testPath := strings.TrimPrefix(ent.Path, savePath)
testPath = filepath.Join(localDir, testPath)
logger.Verboseln("同步删除检测:", testPath, ent.Path)
//为防止误删,只有当 err 是文件不存在的时候才进行删除处理。
if fi, err := os.Stat(testPath); err == nil || !os.IsNotExist(err) {
//使用sync功能时没有传时间参数进来为方便对比回写数据库需补上时间。
if fi != nil {
ent.ModTime = fi.ModTime().Unix()
}
return true
}
var err *apierror.ApiError
// 尝试从本地数据库查找
if ent.ParentId == "" {
if test := db.Get(path.Dir(ent.Path)); test != nil && test.IsFolder && test.FileId != "" {
ent.ParentId = test.FileId
}
}
// 从网盘查找
if ent.FileId == "" || ent.ParentId == "" {
efi, err := activeUser.PanClient().FileInfoById(driveId, ent.FileId)
//网盘上不存在这个文件或目录,只需要清理数据库
if err != nil && err.Code == apierror.ApiCodeFileNotFoundCode {
db.DelWithPrefix(ent.Path)
logger.Verboseln("删除数据库记录", ent.Path)
return
}
if efi != nil {
ent.FileId = efi.FileId
ent.ParentId = efi.ParentFileId
}
}
if ent.FileId == "" {
return
}
// 本地文件不存在
// 删除网盘对应文件
fileDeleteResult, err := activeUser.PanClient().FileDelete([]*aliyunpan.FileBatchActionParam{{DriveId:driveId, FileId:ent.FileId}})
if err != nil || len(fileDeleteResult) == 0 {
fmt.Println("删除网盘文件或目录失败", ent.Path, err)
} else {
db.DelWithPrefix(ent.Path)
logger.Verboseln("删除网盘文件和数据库记录", ent.Path)
}
return
}
// 根据数据库记录删除不存在的文件
if !flagSync {
for ent, err := db.First(savePath); err == nil; ent, err = db.Next(savePath) {
isLocalFileExist(ent)
}
return
}
parent := db.Get(savePath)
if parent.FileId == "" {
efi, err := activeUser.PanClient().FileInfoByPath(driveId, savePath)
if err != nil {
return
}
parent.FileId = efi.FileId
}
var syncFunc func(curPath, parentID string)
syncFunc = func(curPath, parentID string) {
param := &aliyunpan.FileListParam{
DriveId: driveId,
ParentFileId: parentID,
}
fileResult, err := activeUser.PanClient().FileListGetAll(param)
if err != nil {
return
}
if fileResult == nil || len(fileResult) == 0 {
return
}
for _, fileEntity := range fileResult {
ufm := &panupload.UploadedFileMeta{
FileId: fileEntity.FileId,
ParentId: fileEntity.ParentFileId,
Size: fileEntity.FileSize,
IsFolder: fileEntity.IsFolder(),
Path: path.Join(curPath, fileEntity.FileName),
SHA1: strings.ToLower(fileEntity.ContentHash),
}
if !isLocalFileExist(ufm) {
continue
}
//如果这是一个目录就直接更新数据库否则判断原始记录的Hash信息如果一致才更新。
if ufm.IsFolder {
db.Put(ufm.Path, ufm)
syncFunc(ufm.Path, ufm.FileId)
} else if test := db.Get(ufm.Path); test.SHA1 == ufm.SHA1 {
db.Put(ufm.Path, ufm)
}
}
}
//开启自动清理功能
db.AutoClean(parent.Path, true)
db.Put(parent.Path, parent)
syncFunc(savePath, parent.FileId)
}
func checkPath(localdir string) (string, error) {
fullPath, err := filepath.Abs(localdir)
if err != nil {
fullPath = localdir
}
if fi, err := os.Stat(fullPath); err != nil && !fi.IsDir() {
return fullPath, os.ErrInvalid
}
dbpath := filepath.Join(fullPath, BackupMetaDirName)
//数据库目录判断
fi, err := os.Stat(dbpath)
if err != nil {
if os.IsNotExist(err) {
err = os.Mkdir(dbpath, 0755)
}
if err != nil {
return fullPath, fmt.Errorf("数据库目录[%s]创建失败,跳过处理: %s", dbpath, err)
}
}
if fi != nil && !fi.IsDir() {
return fullPath, os.ErrPermission
}
return fullPath, nil
}
func Backup(c *cli.Context) error {
if c.NArg() < 2 {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
}
subArgs := c.Args()
localpaths := make([]string, 0)
flagSync := c.Bool("sync")
flagDelete := c.Bool("delete")
opt := &UploadOptions{
AllParallel: c.Int("p"),
Parallel: 1, // 阿里云盘一个文件只支持单线程上传
MaxRetry: c.Int("retry"),
NoRapidUpload: c.Bool("norapid"),
ShowProgress: !c.Bool("np"),
IsOverwrite: true,
DriveId: parseDriveId(c),
ExcludeNames: c.StringSlice("exn"),
BlockSize: int64(c.Int("bs") * 1024),
}
localCount := c.NArg() - 1
savePath := GetActiveUser().PathJoin(opt.DriveId, subArgs[localCount])
wg := sync.WaitGroup{}
wg.Add(localCount)
for _, p := range subArgs[:localCount] {
go func(p string) {
defer wg.Done()
fullPath, err := checkPath(p)
switch err {
case nil:
if flagSync || flagDelete {
DelRemoteFileFromDB(opt.DriveId, fullPath, savePath, flagSync)
}
case os.ErrInvalid:
default:
return
}
localpaths = append(localpaths, fullPath)
}(p)
}
wg.Wait()
if len(localpaths) == 0 {
return nil
}
RunUpload(localpaths, savePath, opt)
return nil
}