remove webdav code

This commit is contained in:
tickstep
2024-03-01 11:35:18 +08:00
parent 4e8c4d6e9c
commit c350cadba4
13 changed files with 1 additions and 1900 deletions
-1
View File
@@ -354,5 +354,4 @@ Web网页端
# 鸣谢
本项目大量借鉴了以下相关项目的功能&成果
> [tickstep/cloudpan189-go](https://github.com/tickstep/cloudpan189-go)
> [hacdias/webdav](https://github.com/hacdias/webdav)
> [kazutoiris/ali_ecc](https://github.com/kazutoiris/ali_ecc)
-59
View File
@@ -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
-25
View File
@@ -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}
-33
View File
@@ -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=
-14
View File
@@ -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
-234
View File
@@ -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,
},
},
},
},
}
}
-7
View File
@@ -1,7 +0,0 @@
package webdav
const (
KeySessionId = "sessionId"
KeyContentLength = "contentLength"
KeyUserId = "userId"
)
-750
View File
@@ -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, &param)
_, 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
}
-50
View File
@@ -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
}
-33
View File
@@ -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
}
-205
View File
@@ -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)
}
-91
View File
@@ -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)
}
}
-397
View File
@@ -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
}
}