mirror of
https://gitee.com/zhangsken/aliyunpan.git
synced 2026-10-16 10:29:42 +08:00
remove webdav code
This commit is contained in:
@@ -353,6 +353,5 @@ Web网页端
|
||||
|
||||
# 鸣谢
|
||||
本项目大量借鉴了以下相关项目的功能&成果
|
||||
> [tickstep/cloudpan189-go](https://github.com/tickstep/cloudpan189-go)
|
||||
> [hacdias/webdav](https://github.com/hacdias/webdav)
|
||||
> [tickstep/cloudpan189-go](https://github.com/tickstep/cloudpan189-go)
|
||||
> [kazutoiris/ali_ecc](https://github.com/kazutoiris/ali_ecc)
|
||||
@@ -1,59 +0,0 @@
|
||||
# alpine:3.15.0
|
||||
|
||||
# 参数
|
||||
ARG DOCKER_IMAGE_HASH
|
||||
|
||||
FROM alpine@sha256:$DOCKER_IMAGE_HASH
|
||||
LABEL author="tickstep"
|
||||
LABEL email="tickstep@outlook.com"
|
||||
LABEL version="1.0"
|
||||
LABEL description="webdav file server for aliyun cloud drive"
|
||||
|
||||
# 时区
|
||||
ENV TZ=Asia/Shanghai
|
||||
# 手动下载tzdata安装包,注意要下载对应架构的: https://dl-cdn.alpinelinux.org/alpine/v3.15/community/
|
||||
RUN apk add -U tzdata
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
|
||||
# 创建运行目录
|
||||
RUN mkdir -p /home/app
|
||||
VOLUME /home/app
|
||||
WORKDIR /home/app
|
||||
RUN cd /home/app
|
||||
|
||||
# 创建配置文件目录
|
||||
RUN mkdir -p /home/app/config
|
||||
|
||||
# 复制文件
|
||||
COPY ./docker/webdav/app.sh /home/app/app.sh
|
||||
RUN chmod +x /home/app/app.sh
|
||||
COPY ./docker/webdav/health_check.sh /home/app/health_check.sh
|
||||
RUN chmod +x /home/app/health_check.sh
|
||||
|
||||
COPY ./out/binary_files/aliyunpan /home/app
|
||||
RUN mkdir -p /home/app/config/plugin
|
||||
COPY ./out/binary_files/plugin /home/app/config/plugin
|
||||
RUN mkdir -p /home/app/config/sync_drive
|
||||
COPY ./out/binary_files/sync_drive /home/app/config/sync_drive
|
||||
#RUN chmod +x /home/app/aliyunpan
|
||||
|
||||
# 健康检查
|
||||
HEALTHCHECK --start-period=5s --interval=10s --timeout=5s --retries=3 CMD /bin/sh /home/app/health_check.sh
|
||||
|
||||
# 端口
|
||||
EXPOSE 23077
|
||||
|
||||
# 环境变量
|
||||
ENV ALIYUNPAN_DOCKER=1
|
||||
ENV ALIYUNPAN_CONFIG_DIR=/home/app/config
|
||||
ENV ALIYUNPAN_AUTH_USER=admin
|
||||
ENV ALIYUNPAN_AUTH_PASSWORD=admin
|
||||
ENV ALIYUNPAN_WEBDAV_MODE=rw
|
||||
ENV ALIYUNPAN_PAN_DRIVE=File
|
||||
ENV ALIYUNPAN_PAN_DIR=/
|
||||
ENV ALIYUNPAN_REFRESH_TOKEN=""
|
||||
ENV ALIYUNPAN_TRANSFER_URL_TYPE=0
|
||||
ENV ALIYUNPAN_BLOCK_SIZE=10240
|
||||
ENV ALIYUNPAN_DEVICE_ID=""
|
||||
|
||||
ENTRYPOINT ./app.sh
|
||||
@@ -1,25 +0,0 @@
|
||||
#!/bin/sh
|
||||
cd /home/app
|
||||
chmod +x ./aliyunpan
|
||||
|
||||
# device-id
|
||||
if [[ -z $ALIYUNPAN_DEVICE_ID ]];
|
||||
then
|
||||
echo "the program use random device id"
|
||||
else
|
||||
echo "set device id"
|
||||
./aliyunpan config set -device_id ${ALIYUNPAN_DEVICE_ID}
|
||||
fi
|
||||
|
||||
# check login already or not
|
||||
./aliyunpan who
|
||||
if [ $? -eq 0 ]
|
||||
then
|
||||
echo "cache token is valid, not need to re-login"
|
||||
else
|
||||
echo "login use refresh token: ${ALIYUNPAN_REFRESH_TOKEN}"
|
||||
./aliyunpan login -RefreshToken=${ALIYUNPAN_REFRESH_TOKEN}
|
||||
fi
|
||||
|
||||
./aliyunpan config set -transfer_url_type ${ALIYUNPAN_TRANSFER_URL_TYPE}
|
||||
./aliyunpan webdav start -ip "0.0.0.0" -port 23077 -webdav_user "${ALIYUNPAN_AUTH_USER}" -webdav_password "${ALIYUNPAN_AUTH_PASSWORD}" -webdav_mode "${ALIYUNPAN_WEBDAV_MODE}" -pan_drive "${ALIYUNPAN_PAN_DRIVE}" -pan_dir_path "${ALIYUNPAN_PAN_DIR}" -bs ${ALIYUNPAN_BLOCK_SIZE}
|
||||
@@ -1,33 +0,0 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
webdav:
|
||||
image: tickstep/aliyunpan-webdav:v0.1.6
|
||||
container_name: aliyunpan-webdav
|
||||
restart: always
|
||||
ports:
|
||||
- 23077:23077
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
# refresh token
|
||||
- ALIYUNPAN_REFRESH_TOKEN=4dd40e8b165...42b8833e4a507b6
|
||||
# webdav 登录用户名
|
||||
- ALIYUNPAN_AUTH_USER=admin
|
||||
# webdav 登录密码
|
||||
- ALIYUNPAN_AUTH_PASSWORD=admin
|
||||
# webdav 网盘模式,支持:rw-读写,ro-只读
|
||||
- ALIYUNPAN_WEBDAV_MODE=rw
|
||||
# 指定网盘类型为文件,可选: File-文件 Album-相册
|
||||
- ALIYUNPAN_PAN_DRIVE=File
|
||||
# 指定网盘文件夹作为webdav服务根目录
|
||||
- ALIYUNPAN_PAN_DIR=/
|
||||
# 上传下载链接类型:1-默认 2-阿里ECS环境
|
||||
- ALIYUNPAN_TRANSFER_URL_TYPE=1
|
||||
# 上传数据块大小,单位为KB,默认为10240KB,建议范围1024KB~10240KB
|
||||
- ALIYUNPAN_BLOCK_SIZE=10240
|
||||
# 登录设备的ID,默认为空,程序会自动生成一个ID
|
||||
- ALIYUNPAN_DEVICE_ID=
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# 检查webdav的服务端口是否存在
|
||||
netstat -luntp | awk '{print $4}' | grep 23077
|
||||
if [ $? == 0 ]
|
||||
then
|
||||
echo $?
|
||||
exit 0
|
||||
else
|
||||
echo $?
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,234 +0,0 @@
|
||||
// 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/cmder"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/internal/webdav"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/urfave/cli"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
func CmdWebdav() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "webdav",
|
||||
Usage: "在线网盘服务",
|
||||
Description: "webdav在线网盘服务",
|
||||
Category: "阿里云盘",
|
||||
Before: ReloadConfigFunc,
|
||||
After: SaveConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
fmt.Print(`
|
||||
本文命令可以让阿里云盘变身为webdav协议的文件服务器。这样你可以把阿里云盘挂载为Windows、Linux、Mac系统的磁盘,可以通过NAS系统做文件管理或文件同步等等。
|
||||
当把阿里云盘作为webdav文件服务器进行使用的时候,上传文件是不支持秒传的,所以当你挂载为网络磁盘使用的时候,不建议在webdav挂载目录中上传、下载过大的文件,不然体验会非常差。
|
||||
建议作为文档,图片等小文件的同步网盘。
|
||||
|
||||
请输入以下命令查看如何启动
|
||||
aliyunpan webdav start -h
|
||||
|
||||
`)
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
},
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Name: "start",
|
||||
Usage: "启动webdav在线网盘服务",
|
||||
UsageText: cmder.App().Name + " webdav start [arguments...]",
|
||||
Description: `
|
||||
启动webdav服务,让阿里云盘变身为webdav协议的文件服务器。这样你可以把阿里云盘挂载为Windows、Linux、Mac系统的磁盘,可以通过NAS系统做文件管理或文件同步等等。
|
||||
当把阿里云盘作为webdav文件服务器进行使用的时候,上传文件是不支持秒传的,所以当你挂载为网络磁盘使用的时候,不建议在webdav挂载目录中上传、下载过大的文件,不然体验会非常差。
|
||||
建议作为文档,图片等小文件的同步网盘。
|
||||
|
||||
例子:
|
||||
1. 查看帮助
|
||||
aliyunpan webdav start -h
|
||||
|
||||
2. 使用默认配置启动webdav服务
|
||||
aliyunpan webdav start
|
||||
|
||||
3. 启动webdav服务,并配置IP为127.0.0.1,端口为23077,登录用户名为admin,登录密码为admin123,模式为读写,文件网盘目录 /webdav_folder 作为服务的根目录
|
||||
aliyunpan webdav start -ip "127.0.0.1" -port 23077 -webdav_user "admin" -webdav_password "admin123" -webdav_mode "rw" -pan_drive "File" -pan_dir_path "/webdav_folder"
|
||||
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号,请先登录")
|
||||
return nil
|
||||
}
|
||||
activeUser := GetActiveUser()
|
||||
|
||||
// pan token expired checker
|
||||
continueFlag := int32(0)
|
||||
atomic.StoreInt32(&continueFlag, 0)
|
||||
defer func() {
|
||||
atomic.StoreInt32(&continueFlag, 1)
|
||||
}()
|
||||
go func(flag *int32) {
|
||||
for atomic.LoadInt32(flag) == 0 {
|
||||
// token刷新
|
||||
time.Sleep(time.Duration(1) * time.Minute)
|
||||
//time.Sleep(time.Duration(5) * time.Second)
|
||||
if RefreshTokenInNeed(activeUser, config.Config.DeviceName) {
|
||||
logger.Verboseln("reload new access token for webdav")
|
||||
}
|
||||
}
|
||||
}(&continueFlag)
|
||||
|
||||
webdavServ := &webdav.WebdavConfig{
|
||||
PanDriveId: "",
|
||||
PanUserId: "",
|
||||
PanUser: nil,
|
||||
UploadChunkSize: c.Int("bs") * 1024,
|
||||
TransferUrlType: config.Config.TransferUrlType,
|
||||
Address: "0.0.0.0",
|
||||
Port: 23077,
|
||||
Prefix: "/",
|
||||
Users: []webdav.WebdavUser{{
|
||||
Username: "admin",
|
||||
Password: "admin",
|
||||
Scope: "/",
|
||||
Mode: "rw",
|
||||
}},
|
||||
}
|
||||
|
||||
// pan user
|
||||
panUserId := activeUser.UserId
|
||||
webdavServ.PanUserId = panUserId
|
||||
webdavServ.PanUser = activeUser
|
||||
|
||||
// address
|
||||
ip := "0.0.0.0"
|
||||
if c.IsSet("ip") {
|
||||
ip = c.String("ip")
|
||||
}
|
||||
webdavServ.Address = ip
|
||||
|
||||
// port
|
||||
port := 23077
|
||||
if c.IsSet("port") {
|
||||
port = c.Int("port")
|
||||
}
|
||||
webdavServ.Port = port
|
||||
|
||||
// binding pan drive
|
||||
panDriveName := "File"
|
||||
panDriveNameStr := "文件"
|
||||
if c.IsSet("pan_drive") {
|
||||
panDriveName = c.String("pan_drive")
|
||||
}
|
||||
if strings.ToLower(panDriveName) == "album" {
|
||||
webdavServ.PanDriveId = activeUser.DriveList.GetAlbumDriveId()
|
||||
panDriveNameStr = "相册"
|
||||
} else {
|
||||
webdavServ.PanDriveId = activeUser.DriveList.GetFileDriveId()
|
||||
panDriveNameStr = "文件"
|
||||
}
|
||||
|
||||
// binding pan dir path
|
||||
panDirPath := "/"
|
||||
if c.IsSet("pan_dir_path") {
|
||||
panDirPath = c.String("pan_dir_path")
|
||||
}
|
||||
webdavServ.Users[0].Scope = panDirPath
|
||||
|
||||
webdavUserName := "admin"
|
||||
if c.IsSet("webdav_user") {
|
||||
webdavUserName = c.String("webdav_user")
|
||||
}
|
||||
webdavServ.Users[0].Username = webdavUserName
|
||||
|
||||
webdavPassword := "admin"
|
||||
if c.IsSet("webdav_password") {
|
||||
webdavPassword = c.String("webdav_password")
|
||||
}
|
||||
webdavServ.Users[0].Password = webdavPassword
|
||||
|
||||
webdavMode := "rw"
|
||||
if c.IsSet("webdav_mode") {
|
||||
webdavMode = c.String("webdav_mode")
|
||||
}
|
||||
webdavServ.Users[0].Mode = webdavMode
|
||||
if webdavServ.Users[0].Mode != "rw" && webdavServ.Users[0].Mode != "ro" {
|
||||
webdavServ.Users[0].Mode = "rw"
|
||||
}
|
||||
|
||||
err := config.Config.Save()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return err
|
||||
}
|
||||
|
||||
modeStr := "读写"
|
||||
if webdavServ.Users[0].Mode == "rw" {
|
||||
modeStr = "读写"
|
||||
} else if webdavServ.Users[0].Mode == "ro" {
|
||||
modeStr = "只读"
|
||||
}
|
||||
|
||||
fmt.Println("----------------------------------------")
|
||||
fmt.Printf("webdav网盘信息:\n链接:http://localhost:%d\n用户名:%s\n密码:%s\n网盘模式:%s\n网盘服务类型:%s\n网盘服务目录:%s\n",
|
||||
webdavServ.Port, webdavServ.Users[0].Username, webdavServ.Users[0].Password, modeStr, panDriveNameStr, webdavServ.Users[0].Scope)
|
||||
fmt.Println("----------------------------------------")
|
||||
fmt.Println("webdav在线网盘服务运行中...")
|
||||
webdavServ.StartServer()
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "ip",
|
||||
Usage: "绑定的本地IP,多网卡的环境下建议指定绑定的IP。默认为0.0.0.0代表绑定全部网卡",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "port",
|
||||
Usage: "绑定的本地端口,默认为:23077",
|
||||
Value: 23077,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "webdav_user",
|
||||
Usage: "Webdav登录用户名,默认为:admin",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "webdav_password",
|
||||
Usage: "Webdav登录密码,默认为:admin",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "webdav_mode",
|
||||
Usage: "Webdav模式,包括:rw-读写,ro-只读,默认为:rw",
|
||||
Value: "rw",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "pan_drive",
|
||||
Usage: "Webdav绑定的网盘类型。File-文件 Album-相册。默认为文件网盘",
|
||||
Value: "File",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "pan_dir_path",
|
||||
Usage: "Webdav绑定的网盘文件夹路径,默认为:/",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "bs",
|
||||
Usage: "block size,上传分片大小,单位KB。推荐值:1024 ~ 10240",
|
||||
Value: 10240,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package webdav
|
||||
|
||||
const (
|
||||
KeySessionId = "sessionId"
|
||||
KeyContentLength = "contentLength"
|
||||
KeyUserId = "userId"
|
||||
)
|
||||
@@ -1,750 +0,0 @@
|
||||
package webdav
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/library-go/expires"
|
||||
"github.com/tickstep/library-go/expires/cachemap"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/tickstep/library-go/requester"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type FileDownloadStream struct {
|
||||
readOffset int64
|
||||
resp *http.Response
|
||||
timestamp int64
|
||||
}
|
||||
|
||||
type FileUploadStream struct {
|
||||
fileUploadInfoEntity *aliyunpan.CreateFileUploadResult
|
||||
|
||||
filePath string
|
||||
fileSize int64
|
||||
fileId string
|
||||
fileWritePos int64
|
||||
fileUploadUrlIndex int
|
||||
|
||||
chunkBuffer []byte
|
||||
chunkPos int64
|
||||
chunkSize int64
|
||||
|
||||
timestamp int64
|
||||
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
type PanClientProxy struct {
|
||||
PanUser *config.PanUser
|
||||
PanDriveId string
|
||||
PanTransferUrlType int
|
||||
|
||||
mutex sync.Mutex
|
||||
|
||||
// 网盘文件路径到网盘文件信息实体映射缓存
|
||||
filePathCacheMap cachemap.CacheOpMap
|
||||
|
||||
// 网盘文件夹路径到文件夹下面所有子文件映射缓存
|
||||
fileDirectoryListCacheMap cachemap.CacheOpMap
|
||||
|
||||
// 网盘文件ID到文件下载链接映射缓存
|
||||
fileIdDownloadUrlCacheMap cachemap.CacheOpMap
|
||||
|
||||
// 网盘文件ID到文件下载数据流映射缓存
|
||||
fileIdDownloadStreamCacheMap cachemap.CacheOpMap
|
||||
|
||||
// 网盘文件到文件上传数据流映射缓存
|
||||
filePathUploadStreamCacheMap cachemap.CacheOpMap
|
||||
}
|
||||
|
||||
// DefaultChunkSize 默认上传的文件块大小,10MB
|
||||
const DefaultChunkSize = 10 * 1024 * 1024
|
||||
|
||||
// CacheExpiredMinute 缓存过期分钟
|
||||
const CacheExpiredMinute = 60
|
||||
|
||||
// FileDownloadUrlExpiredSeconds 文件下载URL过期时间
|
||||
const FileDownloadUrlExpiredSeconds = 14400
|
||||
|
||||
// FileUploadExpiredMinute 文件上传数据流过期时间
|
||||
const FileUploadExpiredMinute = 1440 // 24小时
|
||||
|
||||
// getDownloadFileUrl 获取文件下载URL
|
||||
func (p *PanClientProxy) getFileDownloadUrl(urlResult *aliyunpan.GetFileDownloadUrlResult) string {
|
||||
if urlResult == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if p.PanTransferUrlType == 2 { // 阿里ECS内网链接
|
||||
return urlResult.InternalUrl
|
||||
}
|
||||
return urlResult.Url
|
||||
}
|
||||
|
||||
// getFileUploadUrl 获取文件上传URL
|
||||
func (p *PanClientProxy) getFileUploadUrl(urlResult aliyunpan.FileUploadPartInfoResult) string {
|
||||
if p.PanTransferUrlType == 2 { // 阿里ECS内网链接
|
||||
return urlResult.InternalUploadURL
|
||||
}
|
||||
return urlResult.UploadURL
|
||||
}
|
||||
|
||||
// DeleteCache 删除含有 dirs 的缓存
|
||||
func (p *PanClientProxy) deleteFilesDirectoriesListCache(dirs []string) {
|
||||
cache := p.fileDirectoryListCacheMap.LazyInitCachePoolOp(p.PanDriveId)
|
||||
for _, v := range dirs {
|
||||
key := formatPathStyle(v)
|
||||
_, ok := cache.Load(key)
|
||||
if ok {
|
||||
cache.Delete(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteOneCache 删除缓存
|
||||
func (p *PanClientProxy) deleteOneFilesDirectoriesListCache(dirPath string) {
|
||||
dirPath = formatPathStyle(dirPath)
|
||||
ps := []string{dirPath}
|
||||
p.deleteFilesDirectoriesListCache(ps)
|
||||
}
|
||||
|
||||
// cacheFilesDirectoriesList 缓存文件夹下面的所有文件列表
|
||||
func (p *PanClientProxy) cacheFilesDirectoriesList(pathStr string) (fdl aliyunpan.FileList, apiError *apierror.ApiError) {
|
||||
pathStr = formatPathStyle(pathStr)
|
||||
data := p.fileDirectoryListCacheMap.CacheOperation(p.PanDriveId, pathStr, func() expires.DataExpires {
|
||||
fi, er := p.cacheFilePath(pathStr)
|
||||
if er != nil {
|
||||
return nil
|
||||
}
|
||||
fileListParam := &aliyunpan.FileListParam{
|
||||
DriveId: p.PanDriveId,
|
||||
ParentFileId: fi.FileId,
|
||||
Limit: 200,
|
||||
}
|
||||
fdl, apiError = p.PanUser.PanClient().FileListGetAll(fileListParam, 200)
|
||||
if apiError != nil {
|
||||
return nil
|
||||
}
|
||||
if len(fdl) == 0 {
|
||||
// 空目录不缓存
|
||||
return nil
|
||||
}
|
||||
// construct full path
|
||||
for _, f := range fdl {
|
||||
f.Path = path.Join(pathStr, f.FileName)
|
||||
}
|
||||
p.cacheFilePathEntityList(fdl)
|
||||
return expires.NewDataExpires(fdl, CacheExpiredMinute*time.Minute)
|
||||
})
|
||||
if apiError != nil {
|
||||
return
|
||||
}
|
||||
if data == nil {
|
||||
return aliyunpan.FileList{}, nil
|
||||
}
|
||||
return data.Data().(aliyunpan.FileList), nil
|
||||
}
|
||||
|
||||
// deleteOneFilePathCache 删除缓存
|
||||
func (p *PanClientProxy) deleteOneFilePathCache(pathStr string) {
|
||||
key := formatPathStyle(pathStr)
|
||||
cache := p.filePathCacheMap.LazyInitCachePoolOp(p.PanDriveId)
|
||||
_, ok := cache.Load(key)
|
||||
if ok {
|
||||
cache.Delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
// cacheFilePath 缓存文件绝对路径到网盘文件信息
|
||||
func (p *PanClientProxy) cacheFilePath(pathStr string) (fe *aliyunpan.FileEntity, apiError *apierror.ApiError) {
|
||||
pathStr = formatPathStyle(pathStr)
|
||||
data := p.filePathCacheMap.CacheOperation(p.PanDriveId, pathStr, func() expires.DataExpires {
|
||||
var fi *aliyunpan.FileEntity
|
||||
fi, apiError = p.PanUser.PanClient().FileInfoByPath(p.PanDriveId, pathStr)
|
||||
if apiError != nil {
|
||||
return nil
|
||||
}
|
||||
return expires.NewDataExpires(fi, CacheExpiredMinute*time.Minute)
|
||||
})
|
||||
if apiError != nil {
|
||||
return nil, apiError
|
||||
}
|
||||
if data == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return data.Data().(*aliyunpan.FileEntity), nil
|
||||
}
|
||||
|
||||
func (p *PanClientProxy) cacheFilePathEntity(fe *aliyunpan.FileEntity) {
|
||||
pathStr := formatPathStyle(fe.Path)
|
||||
p.filePathCacheMap.CacheOperation(p.PanDriveId, pathStr, func() expires.DataExpires {
|
||||
return expires.NewDataExpires(fe, CacheExpiredMinute*time.Minute)
|
||||
})
|
||||
}
|
||||
|
||||
func (p *PanClientProxy) cacheFilePathEntityList(fdl aliyunpan.FileList) {
|
||||
for _, entity := range fdl {
|
||||
pathStr := formatPathStyle(entity.Path)
|
||||
p.filePathCacheMap.CacheOperation(p.PanDriveId, pathStr, func() expires.DataExpires {
|
||||
return expires.NewDataExpires(entity, CacheExpiredMinute*time.Minute)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// cacheFileDownloadStream 缓存文件下载路径
|
||||
func (p *PanClientProxy) cacheFileDownloadUrl(sessionId, fileId string) (urlResult *aliyunpan.GetFileDownloadUrlResult, apiError *apierror.ApiError) {
|
||||
k := sessionId + "-" + fileId
|
||||
data := p.fileIdDownloadUrlCacheMap.CacheOperation(p.PanDriveId, k, func() expires.DataExpires {
|
||||
urlResult, err1 := p.PanUser.PanClient().GetFileDownloadUrl(&aliyunpan.GetFileDownloadUrlParam{
|
||||
DriveId: p.PanDriveId,
|
||||
FileId: fileId,
|
||||
ExpireSec: FileDownloadUrlExpiredSeconds,
|
||||
})
|
||||
if err1 != nil {
|
||||
return nil
|
||||
}
|
||||
return expires.NewDataExpires(urlResult, (FileDownloadUrlExpiredSeconds-60)*time.Second)
|
||||
})
|
||||
if data == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return data.Data().(*aliyunpan.GetFileDownloadUrlResult), nil
|
||||
}
|
||||
|
||||
// deleteOneFileDownloadStreamCache 删除缓存文件下载流缓存
|
||||
func (p *PanClientProxy) deleteOneFileDownloadStreamCache(sessionId, fileId string) {
|
||||
key := sessionId + "-" + fileId
|
||||
cache := p.fileIdDownloadStreamCacheMap.LazyInitCachePoolOp(p.PanDriveId)
|
||||
_, ok := cache.Load(key)
|
||||
if ok {
|
||||
cache.Delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
// cacheFileDownloadStream 缓存文件下载流
|
||||
func (p *PanClientProxy) cacheFileDownloadStream(sessionId, fileId string, offset int64) (fds *FileDownloadStream, apiError *apierror.ApiError) {
|
||||
k := sessionId + "-" + fileId
|
||||
data := p.fileIdDownloadStreamCacheMap.CacheOperation(p.PanDriveId, k, func() expires.DataExpires {
|
||||
urlResult, err1 := p.cacheFileDownloadUrl(sessionId, fileId)
|
||||
if err1 != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var resp *http.Response
|
||||
var err error
|
||||
var client = requester.NewHTTPClient()
|
||||
// set to no timeout
|
||||
client.Timeout = 0
|
||||
apierr := p.PanUser.PanClient().DownloadFileData(
|
||||
p.getFileDownloadUrl(urlResult),
|
||||
aliyunpan.FileDownloadRange{
|
||||
Offset: offset,
|
||||
End: 0,
|
||||
},
|
||||
func(httpMethod, fullUrl string, headers map[string]string) (*http.Response, error) {
|
||||
resp, err = client.Req(httpMethod, fullUrl, nil, headers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, err
|
||||
})
|
||||
|
||||
if apierr != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch resp.StatusCode {
|
||||
case 200, 206:
|
||||
// do nothing, continue
|
||||
break
|
||||
case 416: //Requested Range Not Satisfiable
|
||||
fallthrough
|
||||
case 403: // Forbidden
|
||||
fallthrough
|
||||
case 406: // Not Acceptable
|
||||
return nil
|
||||
case 404:
|
||||
return nil
|
||||
case 429, 509: // Too Many Requests
|
||||
return nil
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Verboseln(sessionId + " create new cache for offset = " + strconv.Itoa(int(offset)))
|
||||
return expires.NewDataExpires(&FileDownloadStream{
|
||||
readOffset: offset,
|
||||
resp: resp,
|
||||
timestamp: time.Now().Unix(),
|
||||
}, CacheExpiredMinute*time.Minute)
|
||||
})
|
||||
|
||||
if data == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return data.Data().(*FileDownloadStream), nil
|
||||
}
|
||||
|
||||
// deleteOneFileUploadStreamCache 删除缓存文件下载流缓存
|
||||
func (p *PanClientProxy) deleteOneFileUploadStreamCache(userId, pathStr string) {
|
||||
pathStr = formatPathStyle(pathStr)
|
||||
key := userId + "-" + pathStr
|
||||
cache := p.filePathUploadStreamCacheMap.LazyInitCachePoolOp(p.PanDriveId)
|
||||
_, ok := cache.Load(key)
|
||||
if ok {
|
||||
cache.Delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
// cacheFileUploadStream 缓存创建的文件上传流
|
||||
func (p *PanClientProxy) cacheFileUploadStream(userId, pathStr string, fileSize int64, chunkSize int64) (*FileUploadStream, *apierror.ApiError) {
|
||||
pathStr = formatPathStyle(pathStr)
|
||||
k := userId + "-" + pathStr
|
||||
// TODO: add locker for upload file create
|
||||
data := p.filePathUploadStreamCacheMap.CacheOperation(p.PanDriveId, k, func() expires.DataExpires {
|
||||
// check parent dir is existed or not
|
||||
parentFileId := ""
|
||||
parentFileEntity, err1 := p.cacheFilePath(path.Dir(pathStr))
|
||||
if err1 != nil {
|
||||
return nil
|
||||
}
|
||||
if parentFileEntity == nil {
|
||||
// create parent folder
|
||||
mkr, err2 := p.mkdir(path.Dir(pathStr), 0)
|
||||
if err2 != nil {
|
||||
return nil
|
||||
}
|
||||
parentFileId = mkr.FileId
|
||||
} else {
|
||||
parentFileId = parentFileEntity.FileId
|
||||
}
|
||||
|
||||
// 检查同名文件是否存在
|
||||
efi, apierr := p.PanUser.PanClient().FileInfoByPath(p.PanDriveId, pathStr)
|
||||
if apierr != nil {
|
||||
if apierr.Code == apierror.ApiCodeFileNotFoundCode {
|
||||
// file not existed
|
||||
logger.Verbosef("%s 没有存在同名文件,直接上传: %s\n", userId, pathStr)
|
||||
} else {
|
||||
// TODO: handle error
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
if efi != nil && efi.FileId != "" {
|
||||
// existed, delete it
|
||||
var fileDeleteResult []*aliyunpan.FileBatchActionResult
|
||||
var err *apierror.ApiError
|
||||
fileDeleteResult, err = p.PanUser.PanClient().FileDelete([]*aliyunpan.FileBatchActionParam{{DriveId: efi.DriveId, FileId: efi.FileId}})
|
||||
if err != nil || len(fileDeleteResult) == 0 {
|
||||
logger.Verbosef("%s 同名无法删除文件,请稍后重试: %s\n", userId, pathStr)
|
||||
return nil
|
||||
}
|
||||
time.Sleep(time.Duration(500) * time.Millisecond)
|
||||
logger.Verbosef("%s 检测到同名文件,已移动到回收站: %s\n", userId, pathStr)
|
||||
|
||||
// clear cache
|
||||
p.deleteOneFilePathCache(pathStr)
|
||||
p.deleteOneFilesDirectoriesListCache(path.Dir(pathStr))
|
||||
}
|
||||
}
|
||||
|
||||
// create new upload file
|
||||
appCreateUploadFileParam := &aliyunpan.CreateFileUploadParam{
|
||||
DriveId: p.PanDriveId,
|
||||
Name: filepath.Base(pathStr),
|
||||
Size: fileSize,
|
||||
ContentHash: "",
|
||||
ContentHashName: "none",
|
||||
CheckNameMode: "refuse",
|
||||
ParentFileId: parentFileId,
|
||||
BlockSize: chunkSize,
|
||||
ProofCode: "",
|
||||
ProofVersion: "v1",
|
||||
}
|
||||
|
||||
uploadOpEntity, apierr := p.PanUser.PanClient().CreateUploadFile(appCreateUploadFileParam)
|
||||
if apierr != nil {
|
||||
logger.Verbosef("%s 创建上传任务失败: %s\n", userId, pathStr)
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Verbosef("%s create new upload cache for path = %s\n", userId, pathStr)
|
||||
return expires.NewDataExpires(&FileUploadStream{
|
||||
fileUploadInfoEntity: uploadOpEntity,
|
||||
filePath: pathStr,
|
||||
fileSize: fileSize,
|
||||
fileId: uploadOpEntity.FileId,
|
||||
fileWritePos: 0,
|
||||
fileUploadUrlIndex: 0,
|
||||
chunkBuffer: make([]byte, chunkSize, chunkSize),
|
||||
chunkPos: 0,
|
||||
chunkSize: chunkSize,
|
||||
timestamp: time.Now().Unix(),
|
||||
}, FileUploadExpiredMinute*time.Minute)
|
||||
})
|
||||
|
||||
if data == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return data.Data().(*FileUploadStream), nil
|
||||
}
|
||||
|
||||
// FileInfoByPath 通过文件路径获取网盘文件信息
|
||||
func (p *PanClientProxy) FileInfoByPath(pathStr string) (fileInfo *aliyunpan.FileEntity, error *apierror.ApiError) {
|
||||
return p.cacheFilePath(pathStr)
|
||||
}
|
||||
|
||||
// FileListGetAll 获取文件路径下的所有子文件列表
|
||||
func (p *PanClientProxy) FileListGetAll(pathStr string) (aliyunpan.FileList, *apierror.ApiError) {
|
||||
return p.cacheFilesDirectoriesList(pathStr)
|
||||
}
|
||||
|
||||
func (p *PanClientProxy) mkdir(pathStr string, perm os.FileMode) (*aliyunpan.MkdirResult, error) {
|
||||
pathStr = formatPathStyle(pathStr)
|
||||
r, er := p.PanUser.PanClient().MkdirByFullPath(p.PanDriveId, pathStr)
|
||||
if er != nil {
|
||||
return nil, er
|
||||
}
|
||||
|
||||
// invalidate cache
|
||||
p.deleteOneFilesDirectoriesListCache(path.Dir(pathStr))
|
||||
|
||||
if r.FileId != "" {
|
||||
fe, _ := p.PanUser.PanClient().FileInfoById(p.PanDriveId, r.FileId)
|
||||
if fe != nil {
|
||||
fe.Path = pathStr
|
||||
p.cacheFilePathEntity(fe)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unknown error")
|
||||
}
|
||||
|
||||
// Mkdir 创建目录
|
||||
func (p *PanClientProxy) Mkdir(pathStr string, perm os.FileMode) error {
|
||||
if pathStr == "" {
|
||||
return fmt.Errorf("unknown error")
|
||||
}
|
||||
pathStr = formatPathStyle(pathStr)
|
||||
_, er := p.mkdir(pathStr, perm)
|
||||
return er
|
||||
}
|
||||
|
||||
// Rename 重命名文件
|
||||
func (p *PanClientProxy) Rename(oldpath, newpath string) error {
|
||||
oldpath = formatPathStyle(oldpath)
|
||||
newpath = formatPathStyle(newpath)
|
||||
|
||||
oldFile, er := p.cacheFilePath(oldpath)
|
||||
if er != nil {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
_, e := p.PanUser.PanClient().FileRename(p.PanDriveId, oldFile.FileId, path.Base(newpath))
|
||||
if e != nil {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
// invalidate parent folder cache
|
||||
p.deleteOneFilesDirectoriesListCache(path.Dir(oldpath))
|
||||
|
||||
// add new name cache
|
||||
oldFile.Path = newpath
|
||||
oldFile.FileName = path.Base(newpath)
|
||||
p.cacheFilePathEntity(oldFile)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Move 移动文件
|
||||
func (p *PanClientProxy) Move(oldpath, newpath string) error {
|
||||
oldpath = formatPathStyle(oldpath)
|
||||
newpath = formatPathStyle(newpath)
|
||||
|
||||
oldFile, er := p.cacheFilePath(oldpath)
|
||||
if er != nil {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
|
||||
newFileParentDir, er := p.cacheFilePath(path.Dir(newpath))
|
||||
if er != nil {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
|
||||
param := aliyunpan.FileMoveParam{
|
||||
DriveId: p.PanDriveId,
|
||||
FileId: oldFile.FileId,
|
||||
ToDriveId: p.PanDriveId,
|
||||
ToParentFileId: newFileParentDir.FileId,
|
||||
}
|
||||
params := []*aliyunpan.FileMoveParam{}
|
||||
params = append(params, ¶m)
|
||||
_, e := p.PanUser.PanClient().FileMove(params)
|
||||
if e != nil {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
// invalidate parent folder cache
|
||||
p.deleteOneFilesDirectoriesListCache(path.Dir(oldpath))
|
||||
p.deleteOneFilesDirectoriesListCache(path.Dir(newpath))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DownloadFilePart 下载文件指定数据片段
|
||||
func (p *PanClientProxy) DownloadFilePart(sessionId, fileId string, offset int64, buffer []byte) (int, error) {
|
||||
fds, err1 := p.cacheFileDownloadStream(sessionId, fileId, offset)
|
||||
if err1 != nil {
|
||||
return 0, err1
|
||||
}
|
||||
|
||||
if fds.readOffset != offset {
|
||||
// delete old one
|
||||
if fds.resp != nil {
|
||||
fds.resp.Body.Close()
|
||||
}
|
||||
p.deleteOneFileDownloadStreamCache(sessionId, fileId)
|
||||
logger.Verboseln(sessionId + " offset mismatch offset = " + strconv.Itoa(int(offset)) + " cache offset = " + strconv.Itoa(int(fds.readOffset)))
|
||||
|
||||
// create new one
|
||||
fds, err1 = p.cacheFileDownloadStream(sessionId, fileId, offset)
|
||||
if err1 != nil {
|
||||
return 0, err1
|
||||
}
|
||||
}
|
||||
|
||||
if fds.resp.Close {
|
||||
// delete old one
|
||||
p.deleteOneFileDownloadStreamCache(sessionId, fileId)
|
||||
logger.Verboseln(sessionId + "remote data stream close, stream offset = " + strconv.Itoa(int(fds.readOffset)))
|
||||
|
||||
// create new one
|
||||
fds, err1 = p.cacheFileDownloadStream(sessionId, fileId, offset)
|
||||
if err1 != nil {
|
||||
return 0, err1
|
||||
}
|
||||
}
|
||||
|
||||
readByteCount, readErr := fds.resp.Body.Read(buffer)
|
||||
if readErr != nil {
|
||||
if readErr.Error() == "EOF" {
|
||||
logger.Verboseln(sessionId + " read EOF last offset = " + strconv.Itoa(int(offset)))
|
||||
// end of file
|
||||
if fds.resp != nil {
|
||||
fds.resp.Body.Close()
|
||||
}
|
||||
p.deleteOneFileDownloadStreamCache(sessionId, fileId)
|
||||
} else {
|
||||
// TODO: handler other error
|
||||
return 0, readErr
|
||||
}
|
||||
}
|
||||
fds.readOffset += int64(readByteCount)
|
||||
return readByteCount, nil
|
||||
}
|
||||
|
||||
// RemoveAll 删除文件
|
||||
func (p *PanClientProxy) RemoveAll(pathStr string) error {
|
||||
fi, er := p.FileInfoByPath(pathStr)
|
||||
if er != nil {
|
||||
return er
|
||||
}
|
||||
if fi == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
param := &aliyunpan.FileBatchActionParam{
|
||||
DriveId: p.PanDriveId,
|
||||
FileId: fi.FileId,
|
||||
}
|
||||
_, e := p.PanUser.PanClient().FileDelete(append([]*aliyunpan.FileBatchActionParam{}, param))
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
|
||||
// delete cache
|
||||
p.deleteOneFilesDirectoriesListCache(path.Dir(pathStr))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UploadFilePrepare 创建文件上传
|
||||
func (p *PanClientProxy) UploadFilePrepare(userId, pathStr string, fileSize int64, chunkSize int64) (*FileUploadStream, error) {
|
||||
p.mutex.Lock()
|
||||
defer p.mutex.Unlock()
|
||||
|
||||
cs := chunkSize
|
||||
if cs == 0 {
|
||||
cs = DefaultChunkSize
|
||||
}
|
||||
|
||||
// remove old file cache
|
||||
oldFus, err := p.UploadFileCache(userId, pathStr)
|
||||
if err != nil {
|
||||
logger.Verboseln("query upload file cache error: ", err)
|
||||
}
|
||||
if oldFus != nil {
|
||||
// remove old upload stream cache
|
||||
oldFus.mutex.Lock()
|
||||
p.deleteOneFileUploadStreamCache(userId, pathStr)
|
||||
oldFus.mutex.Unlock()
|
||||
}
|
||||
|
||||
// create new one
|
||||
fus, er := p.cacheFileUploadStream(userId, pathStr, fileSize, cs)
|
||||
if er != nil {
|
||||
return nil, er
|
||||
}
|
||||
return fus, nil
|
||||
}
|
||||
|
||||
func (p *PanClientProxy) UploadFileCache(userId, pathStr string) (*FileUploadStream, error) {
|
||||
key := userId + "-" + formatPathStyle(pathStr)
|
||||
cache := p.filePathUploadStreamCacheMap.LazyInitCachePoolOp(p.PanDriveId)
|
||||
v, ok := cache.Load(key)
|
||||
if ok {
|
||||
return v.Data().(*FileUploadStream), nil
|
||||
}
|
||||
return nil, fmt.Errorf("upload file not found")
|
||||
}
|
||||
|
||||
func (p *PanClientProxy) needToUploadChunk(fus *FileUploadStream) bool {
|
||||
if fus.chunkPos == fus.chunkSize {
|
||||
return true
|
||||
}
|
||||
|
||||
// maybe the final part
|
||||
if fus.fileUploadUrlIndex == (len(fus.fileUploadInfoEntity.PartInfoList) - 1) {
|
||||
finalPartSize := fus.fileSize % fus.chunkSize
|
||||
if finalPartSize == 0 {
|
||||
finalPartSize = fus.chunkSize
|
||||
}
|
||||
if fus.chunkPos == finalPartSize {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isUrlExpired 上传链接是否已过期。过期返回True
|
||||
func (p *PanClientProxy) isUrlExpired(urlStr string) bool {
|
||||
u, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
expiredTimeSecStr := u.Query().Get("x-oss-expires")
|
||||
expiredTimeSec, _ := strconv.ParseInt(expiredTimeSecStr, 10, 64)
|
||||
if (time.Now().Unix() - 10) >= expiredTimeSec {
|
||||
// expired
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// UploadFilePart 上传文件数据块
|
||||
func (p *PanClientProxy) UploadFilePart(userId, pathStr string, offset int64, buffer []byte) (int, error) {
|
||||
fus, err := p.UploadFileCache(userId, pathStr)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
fus.mutex.Lock()
|
||||
defer fus.mutex.Unlock()
|
||||
|
||||
if fus.fileWritePos != offset {
|
||||
// error
|
||||
return 0, fmt.Errorf("file write offset position mismatch")
|
||||
}
|
||||
|
||||
// write buffer to chunk buffer
|
||||
uploadCount := 0
|
||||
for _, b := range buffer {
|
||||
fus.chunkBuffer[fus.chunkPos] = b
|
||||
fus.chunkPos += 1
|
||||
fus.fileWritePos += 1
|
||||
uploadCount += 1
|
||||
|
||||
if p.needToUploadChunk(fus) {
|
||||
// upload chunk to drive
|
||||
uploadBuffer := fus.chunkBuffer
|
||||
if fus.chunkPos < fus.chunkSize {
|
||||
uploadBuffer = make([]byte, fus.chunkPos)
|
||||
copy(uploadBuffer, fus.chunkBuffer)
|
||||
}
|
||||
uploadChunk := bytes.NewReader(uploadBuffer)
|
||||
if fus.fileUploadUrlIndex >= len(fus.fileUploadInfoEntity.PartInfoList) {
|
||||
return uploadCount, fmt.Errorf("upload file uploading status mismatch")
|
||||
}
|
||||
uploadPartInfo := fus.fileUploadInfoEntity.PartInfoList[fus.fileUploadUrlIndex]
|
||||
cd := &aliyunpan.FileUploadChunkData{
|
||||
Reader: uploadChunk,
|
||||
ChunkSize: uploadChunk.Size(),
|
||||
}
|
||||
urlStr := p.getFileUploadUrl(uploadPartInfo)
|
||||
if p.isUrlExpired(urlStr) {
|
||||
// get renew upload url
|
||||
infoList := make([]aliyunpan.FileUploadPartInfoParam, len(fus.fileUploadInfoEntity.PartInfoList))
|
||||
for _, item := range fus.fileUploadInfoEntity.PartInfoList {
|
||||
infoList = append(infoList, aliyunpan.FileUploadPartInfoParam{
|
||||
PartNumber: item.PartNumber,
|
||||
})
|
||||
}
|
||||
refreshUploadParam := &aliyunpan.GetUploadUrlParam{
|
||||
DriveId: fus.fileUploadInfoEntity.DriveId,
|
||||
FileId: fus.fileUploadInfoEntity.FileId,
|
||||
PartInfoList: infoList,
|
||||
UploadId: fus.fileUploadInfoEntity.UploadId,
|
||||
}
|
||||
newUploadInfo, err := p.PanUser.PanClient().GetUploadUrl(refreshUploadParam)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
fus.fileUploadInfoEntity.PartInfoList = newUploadInfo.PartInfoList
|
||||
|
||||
// use new upload url
|
||||
urlStr = p.getFileUploadUrl(fus.fileUploadInfoEntity.PartInfoList[fus.fileUploadUrlIndex])
|
||||
}
|
||||
e := p.PanUser.PanClient().UploadDataChunk(urlStr, cd)
|
||||
if e != nil {
|
||||
// upload error
|
||||
// TODO: handle error, retry upload
|
||||
return uploadCount, nil
|
||||
}
|
||||
fus.fileUploadUrlIndex += 1
|
||||
|
||||
// reset chunk buffer
|
||||
fus.chunkPos = 0
|
||||
}
|
||||
}
|
||||
|
||||
// check file upload completely or not
|
||||
if fus.fileSize == fus.fileWritePos {
|
||||
// complete file upload
|
||||
cufr, err := p.PanUser.PanClient().CompleteUploadFile(&aliyunpan.CompleteUploadFileParam{
|
||||
DriveId: p.PanDriveId,
|
||||
FileId: fus.fileId,
|
||||
UploadId: fus.fileUploadInfoEntity.UploadId,
|
||||
})
|
||||
logger.Verbosef("%s complete upload file: %+v\n", userId, cufr)
|
||||
|
||||
if err != nil {
|
||||
logger.Verbosef("%s complete upload file error: %s\n", userId, err)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// remove cache
|
||||
p.deleteOneFileUploadStreamCache(userId, pathStr)
|
||||
p.deleteOneFilePathCache(pathStr)
|
||||
p.deleteOneFilesDirectoriesListCache(path.Dir(pathStr))
|
||||
}
|
||||
|
||||
return uploadCount, nil
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package webdav
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/webdav"
|
||||
)
|
||||
|
||||
// Rule is a dissalow/allow rule.
|
||||
type Rule struct {
|
||||
Regex bool
|
||||
Allow bool
|
||||
Modify bool
|
||||
Path string
|
||||
Regexp *regexp.Regexp
|
||||
}
|
||||
|
||||
// User contains the settings of each user.
|
||||
type User struct {
|
||||
Username string
|
||||
Password string
|
||||
Scope string
|
||||
Modify bool // 用户是否具备修改权限
|
||||
Rules []*Rule // 根据访问路径进行精细化权限控制
|
||||
Handler *webdav.Handler
|
||||
}
|
||||
|
||||
// Allowed checks if the user has permission to access a directory/file
|
||||
func (u User) Allowed(url string, noModification bool) bool {
|
||||
var rule *Rule
|
||||
i := len(u.Rules) - 1
|
||||
|
||||
for i >= 0 {
|
||||
rule = u.Rules[i]
|
||||
|
||||
isAllowed := rule.Allow && (noModification || rule.Modify)
|
||||
if rule.Regex {
|
||||
if rule.Regexp.MatchString(url) {
|
||||
return isAllowed
|
||||
}
|
||||
} else if strings.HasPrefix(url, rule.Path) {
|
||||
return isAllowed
|
||||
}
|
||||
|
||||
i--
|
||||
}
|
||||
|
||||
return noModification || u.Modify
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package webdav
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func checkPassword(saved, input string) bool {
|
||||
if strings.HasPrefix(saved, "{bcrypt}") {
|
||||
savedPassword := strings.TrimPrefix(saved, "{bcrypt}")
|
||||
return bcrypt.CompareHashAndPassword([]byte(savedPassword), []byte(input)) == nil
|
||||
}
|
||||
|
||||
return saved == input
|
||||
}
|
||||
|
||||
func isAllowedHost(allowedHosts []string, origin string) bool {
|
||||
for _, host := range allowedHosts {
|
||||
if host == origin {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func formatPathStyle(pathStr string) string {
|
||||
pathStr = strings.ReplaceAll(pathStr, "\\", "/")
|
||||
if pathStr != "/" {
|
||||
pathStr = strings.TrimSuffix(pathStr, "/")
|
||||
}
|
||||
return pathStr
|
||||
}
|
||||
@@ -1,205 +0,0 @@
|
||||
package webdav
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"golang.org/x/net/webdav"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CorsCfg is the CORS config.
|
||||
type CorsCfg struct {
|
||||
Enabled bool
|
||||
Credentials bool
|
||||
AllowedHeaders []string
|
||||
AllowedHosts []string
|
||||
AllowedMethods []string
|
||||
ExposedHeaders []string
|
||||
}
|
||||
|
||||
// Config is the configuration of a WebDAV instance.
|
||||
type Config struct {
|
||||
*User
|
||||
Auth bool
|
||||
NoSniff bool
|
||||
Cors CorsCfg
|
||||
Users map[string]*User
|
||||
LogFormat string
|
||||
}
|
||||
|
||||
// ServeHTTP determines if the request is for this plugins, and if all prerequisites are met.
|
||||
func (c *Config) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
u := c.User
|
||||
requestOrigin := r.Header.Get("Origin")
|
||||
|
||||
// Add CORS headers before any operation so even on a 401 unauthorized status, CORS will work.
|
||||
if c.Cors.Enabled && requestOrigin != "" {
|
||||
headers := w.Header()
|
||||
|
||||
allowedHeaders := strings.Join(c.Cors.AllowedHeaders, ", ")
|
||||
allowedMethods := strings.Join(c.Cors.AllowedMethods, ", ")
|
||||
exposedHeaders := strings.Join(c.Cors.ExposedHeaders, ", ")
|
||||
|
||||
allowAllHosts := len(c.Cors.AllowedHosts) == 1 && c.Cors.AllowedHosts[0] == "*"
|
||||
allowedHost := isAllowedHost(c.Cors.AllowedHosts, requestOrigin)
|
||||
|
||||
if allowAllHosts {
|
||||
headers.Set("Access-Control-Allow-Origin", "*")
|
||||
} else if allowedHost {
|
||||
headers.Set("Access-Control-Allow-Origin", requestOrigin)
|
||||
}
|
||||
|
||||
if allowAllHosts || allowedHost {
|
||||
headers.Set("Access-Control-Allow-Headers", allowedHeaders)
|
||||
headers.Set("Access-Control-Allow-Methods", allowedMethods)
|
||||
|
||||
if c.Cors.Credentials {
|
||||
headers.Set("Access-Control-Allow-Credentials", "true")
|
||||
}
|
||||
|
||||
if len(c.Cors.ExposedHeaders) > 0 {
|
||||
headers.Set("Access-Control-Expose-Headers", exposedHeaders)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if r.Method == "OPTIONS" && c.Cors.Enabled && requestOrigin != "" {
|
||||
return
|
||||
}
|
||||
|
||||
// Authentication
|
||||
if c.Auth {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
||||
|
||||
// Gets the correct user for this request.
|
||||
username, password, ok := r.BasicAuth()
|
||||
logger.Verboseln("login attempt", "username = "+username, "remote_address = "+r.RemoteAddr)
|
||||
if !ok {
|
||||
http.Error(w, "Not authorized", 401)
|
||||
return
|
||||
}
|
||||
|
||||
user, ok := c.Users[username]
|
||||
if !ok {
|
||||
http.Error(w, "Not authorized", 401)
|
||||
return
|
||||
}
|
||||
|
||||
if !checkPassword(user.Password, password) {
|
||||
logger.Verboseln("invalid password", "username = "+username, "remote_address = "+r.RemoteAddr)
|
||||
http.Error(w, "Not authorized", 401)
|
||||
return
|
||||
}
|
||||
|
||||
u = user
|
||||
logger.Verboseln("user authorized", "username = "+username)
|
||||
} else {
|
||||
// Even if Auth is disabled, we might want to get
|
||||
// the user from the Basic Auth header. Useful for Caddy
|
||||
// plugins implementation.
|
||||
username, _, ok := r.BasicAuth()
|
||||
if ok {
|
||||
if user, ok := c.Users[username]; ok {
|
||||
u = user
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Checks for user permissions relatively to this PATH.
|
||||
noModification := r.Method == "GET" ||
|
||||
r.Method == "HEAD" ||
|
||||
r.Method == "OPTIONS" ||
|
||||
r.Method == "PROPFIND" ||
|
||||
r.Method == "LOCK" ||
|
||||
r.Method == "UNLOCK"
|
||||
//r.Method == "PUT" ||
|
||||
//r.Method == "MOVE" ||
|
||||
//r.Method == "DELETE"
|
||||
|
||||
if !u.Allowed(r.URL.Path, noModification) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == "HEAD" {
|
||||
w = newResponseWriterNoBody(w)
|
||||
}
|
||||
|
||||
// Excerpt from RFC4918, section 9.4:
|
||||
//
|
||||
// GET, when applied to a collection, may return the contents of an
|
||||
// "index.html" resource, a human-readable view of the contents of
|
||||
// the collection, or something else altogether.
|
||||
//
|
||||
// Get, when applied to collection, will return the same as PROPFIND method.
|
||||
if r.Method == "GET" && strings.HasPrefix(r.URL.Path, u.Handler.Prefix) {
|
||||
info, err := u.Handler.FileSystem.Stat(context.TODO(), strings.TrimPrefix(r.URL.Path, u.Handler.Prefix))
|
||||
if err == nil && info.IsDir() {
|
||||
r.Method = "PROPFIND"
|
||||
|
||||
if r.Header.Get("Depth") == "" {
|
||||
r.Header.Add("Depth", "1")
|
||||
}
|
||||
}
|
||||
}
|
||||
// Runs the WebDAV.
|
||||
u.Handler.LockSystem = webdav.NewMemLS()
|
||||
u.Handler.ServeHTTP(w, addContextValue(r))
|
||||
}
|
||||
|
||||
// addContextValue 增加context键值对
|
||||
func addContextValue(r *http.Request) *http.Request {
|
||||
// add sessionId
|
||||
ctx := context.WithValue(r.Context(), KeySessionId, r.RemoteAddr)
|
||||
|
||||
// add userId
|
||||
username, _, _ := r.BasicAuth()
|
||||
ctx = context.WithValue(ctx, KeyUserId, username)
|
||||
|
||||
// add context length
|
||||
length := r.ContentLength
|
||||
if length == 0 {
|
||||
contentLength := r.Header.Get("content-length")
|
||||
if contentLength == "" {
|
||||
contentLength = r.Header.Get("X-Expected-Entity-Length")
|
||||
if contentLength == "" {
|
||||
contentLength = "0"
|
||||
}
|
||||
}
|
||||
if cl, ok := strconv.ParseInt(contentLength, 10, 64); ok == nil {
|
||||
length = cl
|
||||
}
|
||||
}
|
||||
ctx = context.WithValue(ctx, KeyContentLength, length)
|
||||
|
||||
req := r.WithContext(ctx)
|
||||
return req
|
||||
}
|
||||
|
||||
// responseWriterNoBody is a wrapper used to suprress the body of the response
|
||||
// to a request. Mainly used for HEAD requests.
|
||||
type responseWriterNoBody struct {
|
||||
http.ResponseWriter
|
||||
}
|
||||
|
||||
// newResponseWriterNoBody creates a new responseWriterNoBody.
|
||||
func newResponseWriterNoBody(w http.ResponseWriter) *responseWriterNoBody {
|
||||
return &responseWriterNoBody{w}
|
||||
}
|
||||
|
||||
// Header executes the Header method from the http.ResponseWriter.
|
||||
func (w responseWriterNoBody) Header() http.Header {
|
||||
return w.ResponseWriter.Header()
|
||||
}
|
||||
|
||||
// Write suprresses the body.
|
||||
func (w responseWriterNoBody) Write(data []byte) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// WriteHeader writes the header to the http.ResponseWriter.
|
||||
func (w responseWriterNoBody) WriteHeader(statusCode int) {
|
||||
w.ResponseWriter.WriteHeader(statusCode)
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
package webdav
|
||||
|
||||
import (
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"golang.org/x/net/webdav"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type WebdavUser struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Scope string `json:"scope"`
|
||||
Mode string `json:"mode"`
|
||||
}
|
||||
|
||||
type WebdavConfig struct {
|
||||
// 指定Webdav使用哪个账号的云盘资源
|
||||
PanUserId string `json:"panUserId"`
|
||||
PanDriveId string `json:"panDriveId"`
|
||||
PanUser *config.PanUser `json:"-"`
|
||||
UploadChunkSize int `json:"uploadChunkSize"` // 上传文件分片数据块大小,该数值不能太小,建议大于等于512KB
|
||||
TransferUrlType int `json:"transferUrlType"` // 上传/下载URL类别,1-默认,2-阿里云ECS
|
||||
|
||||
Address string `json:"address"`
|
||||
Port int `json:"port"`
|
||||
Prefix string `json:"prefix"`
|
||||
Users []WebdavUser `json:"users"`
|
||||
}
|
||||
|
||||
func (w *WebdavConfig) StartServer() {
|
||||
users := map[string]*User{}
|
||||
for _, u := range w.Users {
|
||||
fileItem, e := w.PanUser.PanClient().FileInfoByPath(w.PanDriveId, u.Scope)
|
||||
if e != nil {
|
||||
logger.Verboseln("scope not existed, shutting server")
|
||||
return
|
||||
}
|
||||
wdfi := NewWebDavFileInfo(fileItem)
|
||||
if wdfi.fullPath != "/" && strings.Index(wdfi.fullPath, "/") != 0 {
|
||||
wdfi.fullPath = "/" + wdfi.fullPath
|
||||
}
|
||||
panClientProxy := &PanClientProxy{
|
||||
PanUser: w.PanUser,
|
||||
PanDriveId: w.PanDriveId,
|
||||
PanTransferUrlType: w.TransferUrlType,
|
||||
}
|
||||
users[u.Username] = &User{
|
||||
Username: u.Username,
|
||||
Password: u.Password,
|
||||
Scope: u.Scope,
|
||||
Modify: u.Mode == "rw",
|
||||
Rules: nil,
|
||||
Handler: &webdav.Handler{
|
||||
Prefix: w.Prefix,
|
||||
FileSystem: WebDavDir{
|
||||
Dir: webdav.Dir(u.Scope),
|
||||
NoSniff: false,
|
||||
panClientProxy: panClientProxy,
|
||||
fileInfo: wdfi,
|
||||
uploadChunkSize: w.UploadChunkSize,
|
||||
},
|
||||
LockSystem: webdav.NewMemLS(),
|
||||
},
|
||||
}
|
||||
// load & cache root folder info
|
||||
_, _ = panClientProxy.FileListGetAll(u.Scope)
|
||||
}
|
||||
cfg := &Config{
|
||||
Auth: true,
|
||||
NoSniff: false,
|
||||
Cors: CorsCfg{
|
||||
Enabled: false,
|
||||
Credentials: false,
|
||||
},
|
||||
Users: users,
|
||||
LogFormat: "",
|
||||
}
|
||||
|
||||
listener, err := net.Listen("tcp", w.Address+":"+strconv.Itoa(w.Port))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := http.Serve(listener, cfg); err != nil {
|
||||
logger.Verboseln("shutting server", err)
|
||||
}
|
||||
}
|
||||
@@ -1,397 +0,0 @@
|
||||
package webdav
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"io"
|
||||
"mime"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/webdav"
|
||||
)
|
||||
|
||||
// NoSniffFileInfo wraps any generic FileInfo interface and bypasses mime type sniffing.
|
||||
type NoSniffFileInfo struct {
|
||||
os.FileInfo
|
||||
}
|
||||
|
||||
func (w NoSniffFileInfo) ContentType(ctx context.Context) (contentType string, err error) {
|
||||
if mimeType := mime.TypeByExtension(path.Ext(w.FileInfo.Name())); mimeType != "" {
|
||||
// We can figure out the mime from the extension.
|
||||
return mimeType, nil
|
||||
} else {
|
||||
// We can't figure out the mime type without sniffing, call it an octet stream.
|
||||
return "application/octet-stream", nil
|
||||
}
|
||||
}
|
||||
|
||||
// 文件系统
|
||||
type WebDavDir struct {
|
||||
webdav.Dir
|
||||
NoSniff bool
|
||||
panClientProxy *PanClientProxy
|
||||
fileInfo WebDavFileInfo
|
||||
uploadChunkSize int
|
||||
}
|
||||
|
||||
// sliceClean is equivalent to but slightly more efficient than
|
||||
// path.Clean("/" + name).
|
||||
func sliceClean(name string) string {
|
||||
if name == "" || name[0] != '/' {
|
||||
name = "/" + name
|
||||
}
|
||||
return path.Clean(name)
|
||||
}
|
||||
|
||||
// formatAbsoluteName 将name名称更改为绝对路径
|
||||
func (d WebDavDir) formatAbsoluteName(pathStr string) string {
|
||||
if strings.Index(pathStr, "/") != 0 {
|
||||
if d.fileInfo.fullPath != "/" {
|
||||
pathStr = d.fileInfo.fullPath + "/" + pathStr
|
||||
} else {
|
||||
pathStr = d.fileInfo.fullPath + pathStr
|
||||
}
|
||||
}
|
||||
return pathStr
|
||||
}
|
||||
|
||||
func (d WebDavDir) getSessionId(ctx context.Context) string {
|
||||
v := ctx.Value(KeySessionId)
|
||||
if v != nil{
|
||||
return v.(string)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (d WebDavDir) getContentLength(ctx context.Context) int64 {
|
||||
v := ctx.Value(KeyContentLength)
|
||||
if v != nil{
|
||||
return v.(int64)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (d WebDavDir) getUserId(ctx context.Context) string {
|
||||
v := ctx.Value(KeyUserId)
|
||||
if v != nil{
|
||||
return v.(string)
|
||||
}
|
||||
return "anonymous"
|
||||
}
|
||||
|
||||
func (d WebDavDir) resolve(name string) string {
|
||||
// This implementation is based on Dir.Open's code in the standard net/http package.
|
||||
if filepath.Separator != '/' && strings.IndexRune(name, filepath.Separator) >= 0 ||
|
||||
strings.Contains(name, "\x00") {
|
||||
return ""
|
||||
}
|
||||
dir := string(d.Dir)
|
||||
if dir == "" {
|
||||
dir = "."
|
||||
}
|
||||
return formatPathStyle(filepath.Join(dir, filepath.FromSlash(sliceClean(name))))
|
||||
}
|
||||
|
||||
func (d WebDavDir) Mkdir(ctx context.Context, name string, perm os.FileMode) error {
|
||||
if name = d.resolve(name); name == "" {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
return d.panClientProxy.Mkdir(name, perm)
|
||||
}
|
||||
|
||||
func (d WebDavDir) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) {
|
||||
if name = d.resolve(name); name == "" {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
name = d.formatAbsoluteName(name)
|
||||
|
||||
//logger.Verbosef("OpenFile file %s flag:\n O_RDONLY=%t\n O_WRONLY=%t\n O_RDWR=%t\n O_APPEND=%t\n O_CREATE=%t\n O_EXCL=%t\n O_SYNC=%t\n O_TRUNC=%t\n",
|
||||
// name,
|
||||
// flag&os.O_RDONLY == 0, flag&os.O_WRONLY != 0, flag&os.O_RDWR != 0, flag&os.O_APPEND != 0,
|
||||
// flag&os.O_CREATE != 0, flag&os.O_EXCL != 0, flag&os.O_SYNC != 0, flag&os.O_TRUNC != 0)
|
||||
|
||||
if name == d.fileInfo.fullPath {
|
||||
return &WebDavFile{
|
||||
panClientProxy: d.panClientProxy,
|
||||
nameSnapshot: d.fileInfo,
|
||||
childrenSnapshot: nil,
|
||||
listPos: 0,
|
||||
readPos: 0,
|
||||
writePos: 0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if flag&(os.O_SYNC|os.O_APPEND) != 0 {
|
||||
// doesn't support these flags
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
|
||||
fileSize := d.getContentLength(ctx)
|
||||
if flag&os.O_CREATE != 0 {
|
||||
if flag&os.O_EXCL != 0 {
|
||||
return nil, os.ErrExist
|
||||
}
|
||||
// create file instance for writing
|
||||
_, e := d.panClientProxy.UploadFilePrepare(d.getUserId(ctx), name, fileSize, int64(d.uploadChunkSize))
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
}
|
||||
if flag&(os.O_WRONLY|os.O_RDWR) != 0 && flag&os.O_TRUNC != 0 {
|
||||
// file must be created ready
|
||||
// get ready to write data to file stream
|
||||
logger.Verboseln("get ready to write data to file stream")
|
||||
fus,err2 := d.panClientProxy.UploadFileCache(d.getUserId(ctx), name)
|
||||
if err2 != nil {
|
||||
return nil, err2
|
||||
}
|
||||
|
||||
// check zero-file or not
|
||||
if fileSize == 0 {
|
||||
// zero file, upload and completely
|
||||
d.panClientProxy.UploadFilePart(d.getUserId(ctx), name, 0, []byte{})
|
||||
}
|
||||
|
||||
return &WebDavFile{
|
||||
panClientProxy: d.panClientProxy,
|
||||
nameSnapshot: WebDavFileInfo{
|
||||
fileId: fus.fileId,
|
||||
name: path.Base(fus.filePath),
|
||||
size: fus.fileSize,
|
||||
mode: 0,
|
||||
modTime: time.Unix(fus.timestamp, 0),
|
||||
fullPath: fus.filePath,
|
||||
},
|
||||
childrenSnapshot: nil,
|
||||
listPos: 0,
|
||||
readPos: 0,
|
||||
writePos: fus.fileWritePos,
|
||||
sessionId: d.getSessionId(ctx),
|
||||
userId: d.getUserId(ctx),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// default action, open file to read
|
||||
fileItem,e := d.panClientProxy.FileInfoByPath(name)
|
||||
if e != nil {
|
||||
logger.Verboseln("OpenFile failed, file path not existed: " + name)
|
||||
return nil, e
|
||||
}
|
||||
wdfi := NewWebDavFileInfo(fileItem)
|
||||
wdfi.fullPath = d.formatAbsoluteName(name)
|
||||
return &WebDavFile{
|
||||
panClientProxy: d.panClientProxy,
|
||||
nameSnapshot: wdfi,
|
||||
childrenSnapshot: nil,
|
||||
listPos: 0,
|
||||
readPos: 0,
|
||||
writePos: 0,
|
||||
sessionId: d.getSessionId(ctx),
|
||||
userId: d.getUserId(ctx),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d WebDavDir) RemoveAll(ctx context.Context, name string) error {
|
||||
if name = d.resolve(name); name == "" {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
if name == filepath.Clean(string(d.Dir)) {
|
||||
// Prohibit removing the virtual root directory.
|
||||
return os.ErrInvalid
|
||||
}
|
||||
return d.panClientProxy.RemoveAll(name)
|
||||
}
|
||||
|
||||
func (d WebDavDir) Rename(ctx context.Context, oldName, newName string) error {
|
||||
if oldName = d.resolve(oldName); oldName == "" {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
if newName = d.resolve(newName); newName == "" {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
if root := filepath.Clean(string(d.Dir)); root == oldName || root == newName {
|
||||
// Prohibit renaming from or to the virtual root directory.
|
||||
return os.ErrInvalid
|
||||
}
|
||||
if path.Dir(oldName) == path.Dir(newName) {
|
||||
// rename
|
||||
return d.panClientProxy.Rename(oldName, newName)
|
||||
} else {
|
||||
// move file
|
||||
return d.panClientProxy.Move(oldName, newName)
|
||||
}
|
||||
}
|
||||
|
||||
func (d WebDavDir) Stat(ctx context.Context, name string) (os.FileInfo, error) {
|
||||
f := &d.fileInfo
|
||||
if name != "" {
|
||||
fileItem,e := d.panClientProxy.FileInfoByPath(d.formatAbsoluteName(name))
|
||||
if e != nil {
|
||||
logger.Verboseln("file path not existed: " + d.formatAbsoluteName(name))
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
*f = NewWebDavFileInfo(fileItem)
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// WebDavFile 文件实例
|
||||
type WebDavFile struct {
|
||||
webdav.File
|
||||
|
||||
// 网盘Client
|
||||
panClientProxy *PanClientProxy
|
||||
|
||||
// nameSnapshot 文件关联的网盘文件信息
|
||||
nameSnapshot WebDavFileInfo
|
||||
|
||||
// childrenSnapshot 目录下的子文件信息列表
|
||||
childrenSnapshot []WebDavFileInfo
|
||||
|
||||
listPos int
|
||||
|
||||
// 读取偏移值
|
||||
readPos int64
|
||||
|
||||
// 写入偏移值
|
||||
writePos int64
|
||||
|
||||
// 会话ID
|
||||
sessionId string
|
||||
|
||||
// 用户ID
|
||||
userId string
|
||||
}
|
||||
|
||||
func (f *WebDavFile) Close() error {
|
||||
f.readPos = 0
|
||||
f.writePos = 0
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *WebDavFile) Read(p []byte) (int, error) {
|
||||
count, err := f.panClientProxy.DownloadFilePart(f.sessionId, f.nameSnapshot.fileId, f.readPos, p)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
f.readPos += int64(count)
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// Readdir 获取文件目录
|
||||
func (f *WebDavFile) Readdir(count int) (fis []os.FileInfo, err error) {
|
||||
if f.childrenSnapshot == nil || len(f.childrenSnapshot) == 0 {
|
||||
fileList, e := f.panClientProxy.FileListGetAll(f.nameSnapshot.fullPath)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
for _,fileItem := range fileList {
|
||||
wdfi := NewWebDavFileInfo(fileItem)
|
||||
wdfi.fullPath = f.nameSnapshot.fullPath + "/" + wdfi.name
|
||||
f.childrenSnapshot = append(f.childrenSnapshot, wdfi)
|
||||
}
|
||||
}
|
||||
|
||||
realCount := count
|
||||
if (f.listPos + realCount) > len(f.childrenSnapshot) {
|
||||
realCount = len(f.childrenSnapshot) - f.listPos
|
||||
}
|
||||
if realCount == 0 {
|
||||
realCount = len(f.childrenSnapshot)
|
||||
}
|
||||
|
||||
fis = make([]os.FileInfo, realCount)
|
||||
idx := 0
|
||||
for idx < realCount {
|
||||
fis[idx] = &f.childrenSnapshot[f.listPos + idx]
|
||||
idx ++
|
||||
}
|
||||
return fis, nil
|
||||
}
|
||||
|
||||
func (f *WebDavFile) Seek(offset int64, whence int) (int64, error) {
|
||||
var abs int64
|
||||
switch whence {
|
||||
case io.SeekStart:
|
||||
abs = offset
|
||||
logger.Verboseln(f.sessionId + " SeekStart offset = " + strconv.Itoa(int(offset)) + " return offset = " + strconv.Itoa(int(abs)))
|
||||
case io.SeekCurrent:
|
||||
abs = f.readPos + offset
|
||||
logger.Verboseln(f.sessionId + " SeekCurrent offset = " + strconv.Itoa(int(offset)) + " return offset = " + strconv.Itoa(int(abs)))
|
||||
case io.SeekEnd:
|
||||
abs = f.nameSnapshot.size + offset
|
||||
logger.Verboseln(f.sessionId + " SeekEnd offset = " + strconv.Itoa(int(offset)) + " return offset = " + strconv.Itoa(int(abs)))
|
||||
default:
|
||||
return 0, errors.New("invalid whence")
|
||||
}
|
||||
if abs < 0 {
|
||||
return 0, os.ErrInvalid
|
||||
}
|
||||
f.readPos = abs
|
||||
return f.readPos, nil
|
||||
}
|
||||
|
||||
func (f *WebDavFile) Stat() (os.FileInfo, error) {
|
||||
return &f.nameSnapshot, nil
|
||||
}
|
||||
|
||||
func (f *WebDavFile) Write(p []byte) (int, error) {
|
||||
count,err := f.panClientProxy.UploadFilePart(f.userId, f.nameSnapshot.fullPath, f.writePos, p)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
f.writePos += int64(count)
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// WebDavFileInfo 文件信息
|
||||
type WebDavFileInfo struct {
|
||||
os.FileInfo
|
||||
fileId string
|
||||
name string
|
||||
size int64
|
||||
mode os.FileMode
|
||||
modTime time.Time
|
||||
fullPath string
|
||||
}
|
||||
|
||||
func NewWebDavFileInfo(fileItem *aliyunpan.FileEntity) WebDavFileInfo {
|
||||
//var LOC, _ = time.LoadLocation("Asia/Shanghai")
|
||||
LOC := time.FixedZone("CST", 8*3600) // 东8区
|
||||
t,_ := time.ParseInLocation("2006-01-02 15:04:05", fileItem.UpdatedAt, LOC)
|
||||
fm := os.FileMode(0)
|
||||
if fileItem.IsFolder() {
|
||||
fm = os.ModeDir
|
||||
}
|
||||
return WebDavFileInfo{
|
||||
fileId: fileItem.FileId,
|
||||
name: fileItem.FileName,
|
||||
size: fileItem.FileSize,
|
||||
mode: fm,
|
||||
modTime: t,
|
||||
fullPath: fileItem.Path,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *WebDavFileInfo) Name() string { return f.name }
|
||||
func (f *WebDavFileInfo) Size() int64 { return f.size }
|
||||
func (f *WebDavFileInfo) Mode() os.FileMode { return f.mode }
|
||||
func (f *WebDavFileInfo) ModTime() time.Time { return f.modTime }
|
||||
func (f *WebDavFileInfo) IsDir() bool { return f.mode.IsDir() }
|
||||
func (f *WebDavFileInfo) Sys() interface{} { return nil }
|
||||
func (f *WebDavFileInfo) ContentType(ctx context.Context) (contentType string, err error) {
|
||||
if mimeType := mime.TypeByExtension(path.Ext(f.Name())); mimeType != "" {
|
||||
// We can figure out the mime from the extension.
|
||||
return mimeType, nil
|
||||
} else {
|
||||
// We can't figure out the mime type without sniffing, call it an octet stream.
|
||||
return "application/octet-stream", nil
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user