1. 文件的上传

文件的上传是以分块的形式上传,文件的哈希值为缓存目录,存储在缓存路径下。当分块上传完毕后,则会合并文件夹,将缓存文件夹删除。

2. 分块上传

文件的分块已经由前端切割好,发送的请求中req.Action字段会赋值为chunk,网盘只用负责接收请求并根据请求中的动作做出相对应的处理。

为保证文件的完整性,会使用文件的哈希值作为缓存路径的文件夹名,并在该路径下生成缓存文件。

cachePath := req.getCachePath(user.UserID)
if err = fs.Mkdir(cachePath); err != nil {
    err = errors.Wrap(err, errors.InternalServerErr)
    return
}

if req.isFileExist(filepath.Join(cachePath, req.chunkNumber)) {
    return
}

tmpFile, err := ioutil.TempFile(filepath.Join(fs.GetRoot(), cachePath), "temp-")
if err != nil {
    err = errors.Wrap(err, errors.InternalServerErr)
    return
}
defer tmpFile.Close()

赋值上传文件的内容,重新命名缓存文件。

3.分块合并

当上传最后一块分块时,req.Action会赋值为merge,插件会根据该字段进行分块合并的操作。

打开缓存路径,统计分块总数是否与请求中的分块总数一致:

fileInfos, err := file.Readdir(-1)
cachePath := req.getCachePath(user.UserID)
file, err := fs.Open(cachePath)

totalChunks, _ := strconv.Atoi(req.TotalChunks)
if len(fileInfos) != totalChunks {
    err = errors.New(status.ChunkFileNotExistErr)
    return
}

创建临时文件,将缓存目录下的所有的缓存文件写入到临时文件中,并检验临时文件的哈希值。

tempFile, err := ioutil.TempFile(filepath.Join(fs.GetRoot(), cachePath), "temp-")
if err != nil {
    err = errors.Wrap(err, errors.InternalServerErr)
    return
}

for i := range fileInfos {
    var chunkFile filebrowser.File
    chunkFile, err = fs.Open(filepath.Join(cachePath, strconv.Itoa(i+1)))
    if err != nil {
        return resp, err
    }
    var b []byte
    b, err = ioutil.ReadAll(chunkFile)
    if err != nil {
        return resp, err
    }
    tempFile.Write(b)
    chunkFile.Close()
}
tempFile.Close()

rootPath := strings.TrimPrefix(tempFile.Name(), fs.GetRoot())
if err = req.checkFileHash(rootPath); err != nil {
    return
}

移动并重命名临时文件,并对其目标目录判断是否需要密钥,如果需要密钥则要对复制后的文件进行加密处理,如果没有就直接将文件复制到目标目录下,删除临时文件:

fileName := strings.TrimPrefix(tempFile.Name(), fs.GetRoot())

// 获取目录的密钥且校验密码,如果密钥为空,则不需要加密,
secret, err := utils.GetFolderSecret(req.path, c.GetHeader("pwd"))
if secret != "" {
    err = fs.CopyFileToTarget(fileName, newPath + types.FolderEncryptExt) // 如果需要的话,需要加上.env文件
    if err != nil {
        ……
        return
    }
    _, err = utils2.EncryptFile(secret, newPath + types.FolderEncryptExt, newPath)
    if err != nil {
        ……
        return
    }
    // 把源文件删除
    _ = fs.Remove(newPath + types.FolderEncryptExt)
} else {
    err = fs.CopyFileToTarget(fileName, newPath)
    if err != nil {
        ……
        return
    }
}
// 成功后删除原来的文件
_ = fs.RemoveAll(fileName)

if err = req.createFolder(newPath, types.FolderTypeFile, user.UserID); err != nil {
    err = errors.Wrap(err, errors.InternalServerErr)
    return
}

resp, err = req.wrapResp(newPath, fs)
if err == nil {
    // 如果合并成功,把分片的文件夹删除
    _ = fs.RemoveAll(cachePath)
}

4. 文件的下载

文件下载需要先判断是否当前用户是有否写权限:

write, _ := c.Get("write")

如果文件有密码,则需要先解密:

if secret != "" {
    ext := strconv.FormatInt(time.Now().UnixNano(), 10)
    downloadPath, err = utils2.DecryptFile(pwd, downloadPath, fmt.Sprint(downloadPath, ".", ext))
    if err != nil {
        return
    }
}

但没有密码就直接打开原文件:

open, err := fb.Open(downloadPath)

最后通过http包中的ServeContent方法传输文件

http.ServeContent(c.Writer, c.Request, fileName, fileInfo.ModTime(), open)

5. 文件的删除

删除文件时,需要判断是否是目录,如果是目录,则需要通过FB将其下的所有文件都删除,否则就根据路径删除文件:

fileInfo, err = fs.Stat(path)
    if err != nil {
        return
    }

if fileInfo.IsDir() {
    if err = fs.RemoveAll(path); err != nil {
        err = errors.Wrap(err, errors.InternalServerErr)
        return
    }
} else {
    if err = fs.Remove(path); err != nil {
        err = errors.Wrap(err, errors.InternalServerErr)
        return
    }
}

最后,删除folder表中的关联信息

if err = entity.DelFolderByAbsPaths(entity.GetDB(), req.Paths); err != nil{}

6.文件的复制和移动

文件进行复制或移动时,需要检查参数,包括req.Action请求是否合法、路径是否合法、以及本次操作的可行性,并获取文件夹密钥和目标文件夹。

if err = req.validateRequest(user.UserID); err != nil{}
secret, err := utils.GetFolderSecret(req.Destination, req.DestinationPwd)
req.Destination, err = utils.GetNewPath(req.Destination)

复制需要FB判断该目标是文件夹还是文件再做复制:

isDir, err := fs.IsDir(path)
if err != nil {
    return
}
if !isDir {
    if err = fs.CopyFile(path, req.Destination); err != nil {
        return
    }
} else {
    if err = fs.CopyDir(path, req.Destination); err != nil {
        return
    }
}

移动文件/文件夹同样是需要FB判断目标是文件夹还是文件,之后先将文件/文件夹复制到目标目录后再将原路径下的删除:

isDir, _ := fs.IsDir(path)
if isDir {
    // 如果是目录, 先复制
    if err = fs.CopyDir(path, req.Destination); err != nil {
        config.Logger.Errorf("resource_operate CopyDir err %v", err)
        return
    }
} else {
    // 如果是文件, 先复制
    if err = fs.CopyFile(path, req.Destination); err != nil {
        config.Logger.Errorf("resource_operate MoveFile err %v", err)
        return
    }
}
if err = filebrowser.GetFB().RemoveAll(path); err != nil {
    config.Logger.Errorf("resource_operate RemoveAll err %v", err)
    return
}
    // 更新原文件的数据,调整路径
newPath := filepath.Join(req.Destination, filepath.Base(path))
if err = utils.UpdateFolderPath(entity.GetDB(), path, newPath); err != nil {
    config.Logger.Errorf("resource_operate UpdateFolderPath err %v", err)
    return
}

最后需要保存数据,并且判断是否需要做加密解密处理:

destPath := filepath.Join(req.Destination, filepath.Base(path))
if err = req.saveFolder(key, uid, secret, destPath); err != nil {
    config.Logger.Errorf("resource_operate save folder err %v", err)
    return
}

7.文件的重命名

文件的重命名比较简单,只需要通过FB重命名,在判断错误信息即可:

if err = fs.Rename(req.Path, newPath); err != nil {
    ……
}