mirror of
				https://github.com/cloudreve/cloudreve.git
				synced 2025-10-31 00:27:31 +08:00 
			
		
		
		
	Feat: import file from existing outer folder
This commit is contained in:
		
							
								
								
									
										2
									
								
								assets
									
									
									
									
									
								
							
							
								
								
								
								
								
							
						
						
									
										2
									
								
								assets
									
									
									
									
									
								
							 Submodule assets updated: 10e5497cfb...df766e4438
									
								
							| @ -222,6 +222,11 @@ func (policy *Policy) IsThumbGenerateNeeded() bool { | |||||||
| 	return policy.Type == "local" | 	return policy.Type == "local" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // CanStructureBeListed 返回存储策略是否能被前台列物理目录 | ||||||
|  | func (policy *Policy) CanStructureBeListed() bool { | ||||||
|  | 	return policy.Type != "local" && policy.Type != "remote" | ||||||
|  | } | ||||||
|  |  | ||||||
| // GetUploadURL 获取文件上传服务API地址 | // GetUploadURL 获取文件上传服务API地址 | ||||||
| func (policy *Policy) GetUploadURL() string { | func (policy *Policy) GetUploadURL() string { | ||||||
| 	server, err := url.Parse(policy.Server) | 	server, err := url.Parse(policy.Server) | ||||||
|  | |||||||
| @ -246,9 +246,11 @@ func TestPolicy_Props(t *testing.T) { | |||||||
| 	asserts.True(policy.IsPathGenerateNeeded()) | 	asserts.True(policy.IsPathGenerateNeeded()) | ||||||
| 	asserts.True(policy.IsTransitUpload(4)) | 	asserts.True(policy.IsTransitUpload(4)) | ||||||
| 	asserts.False(policy.IsTransitUpload(5 * 1024 * 1024)) | 	asserts.False(policy.IsTransitUpload(5 * 1024 * 1024)) | ||||||
|  | 	asserts.True(policy.CanStructureBeListed()) | ||||||
| 	policy.Type = "local" | 	policy.Type = "local" | ||||||
| 	asserts.True(policy.IsThumbGenerateNeeded()) | 	asserts.True(policy.IsThumbGenerateNeeded()) | ||||||
| 	asserts.True(policy.IsPathGenerateNeeded()) | 	asserts.True(policy.IsPathGenerateNeeded()) | ||||||
|  | 	asserts.False(policy.CanStructureBeListed()) | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestPolicy_IsThumbExist(t *testing.T) { | func TestPolicy_IsThumbExist(t *testing.T) { | ||||||
|  | |||||||
| @ -285,6 +285,38 @@ func (fs *FileSystem) List(ctx context.Context, dirPath string, pathProcessor fu | |||||||
| 	return fs.listObjects(ctx, parentPath, childFiles, childFolders, pathProcessor), nil | 	return fs.listObjects(ctx, parentPath, childFiles, childFolders, pathProcessor), nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // ListPhysical 列出存储策略中的外部目录 | ||||||
|  | // TODO:测试 | ||||||
|  | func (fs *FileSystem) ListPhysical(ctx context.Context, dirPath string) ([]Object, error) { | ||||||
|  | 	if err := fs.DispatchHandler(); fs.Policy == nil || err != nil { | ||||||
|  | 		return nil, ErrUnknownPolicyType | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 存储策略不支持列取时,返回空结果 | ||||||
|  | 	if !fs.Policy.CanStructureBeListed() { | ||||||
|  | 		return nil, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 列取路径 | ||||||
|  | 	objects, err := fs.Handler.List(ctx, dirPath, false) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var ( | ||||||
|  | 		folders []model.Folder | ||||||
|  | 	) | ||||||
|  | 	for _, object := range objects { | ||||||
|  | 		if object.IsDir { | ||||||
|  | 			folders = append(folders, model.Folder{ | ||||||
|  | 				Name: object.Name, | ||||||
|  | 			}) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return fs.listObjects(ctx, dirPath, nil, folders, nil), nil | ||||||
|  | } | ||||||
|  |  | ||||||
| func (fs *FileSystem) listObjects(ctx context.Context, parent string, files []model.File, folders []model.Folder, pathProcessor func(string) string) []Object { | func (fs *FileSystem) listObjects(ctx context.Context, parent string, files []model.File, folders []model.Folder, pathProcessor func(string) string) []Object { | ||||||
| 	// 分享文件的ID | 	// 分享文件的ID | ||||||
| 	shareKey := "" | 	shareKey := "" | ||||||
|  | |||||||
| @ -8,14 +8,62 @@ import ( | |||||||
| 	"github.com/HFO4/cloudreve/pkg/cache" | 	"github.com/HFO4/cloudreve/pkg/cache" | ||||||
| 	"github.com/HFO4/cloudreve/pkg/conf" | 	"github.com/HFO4/cloudreve/pkg/conf" | ||||||
| 	"github.com/HFO4/cloudreve/pkg/filesystem/fsctx" | 	"github.com/HFO4/cloudreve/pkg/filesystem/fsctx" | ||||||
|  | 	"github.com/HFO4/cloudreve/pkg/filesystem/response" | ||||||
| 	"github.com/HFO4/cloudreve/pkg/serializer" | 	"github.com/HFO4/cloudreve/pkg/serializer" | ||||||
| 	"github.com/HFO4/cloudreve/pkg/util" | 	"github.com/HFO4/cloudreve/pkg/util" | ||||||
| 	"github.com/jinzhu/gorm" | 	"github.com/jinzhu/gorm" | ||||||
| 	"github.com/stretchr/testify/assert" | 	"github.com/stretchr/testify/assert" | ||||||
|  | 	testMock "github.com/stretchr/testify/mock" | ||||||
| 	"os" | 	"os" | ||||||
| 	"testing" | 	"testing" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | func TestFileSystem_ListPhysical(t *testing.T) { | ||||||
|  | 	asserts := assert.New(t) | ||||||
|  | 	fs := &FileSystem{ | ||||||
|  | 		User: &model.User{ | ||||||
|  | 			Model: gorm.Model{ | ||||||
|  | 				ID: 1, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		Policy: &model.Policy{Type: "mock"}, | ||||||
|  | 	} | ||||||
|  | 	ctx := context.Background() | ||||||
|  |  | ||||||
|  | 	// 未知存储策略 | ||||||
|  | 	{ | ||||||
|  | 		fs.Policy.Type = "unknown" | ||||||
|  | 		res, err := fs.ListPhysical(ctx, "/") | ||||||
|  | 		asserts.Equal(ErrUnknownPolicyType, err) | ||||||
|  | 		asserts.Empty(res) | ||||||
|  | 		fs.Policy.Type = "mock" | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 无法列取目录 | ||||||
|  | 	{ | ||||||
|  | 		testHandler := new(FileHeaderMock) | ||||||
|  | 		testHandler.On("List", testMock.Anything, "/", testMock.Anything).Return([]response.Object{}, errors.New("error")) | ||||||
|  | 		fs.Handler = testHandler | ||||||
|  | 		res, err := fs.ListPhysical(ctx, "/") | ||||||
|  | 		asserts.EqualError(err, "error") | ||||||
|  | 		asserts.Empty(res) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 成功 | ||||||
|  | 	{ | ||||||
|  | 		testHandler := new(FileHeaderMock) | ||||||
|  | 		testHandler.On("List", testMock.Anything, "/", testMock.Anything).Return( | ||||||
|  | 			[]response.Object{{IsDir: true, Name: "1"}, {IsDir: false, Name: "2"}}, | ||||||
|  | 			nil, | ||||||
|  | 		) | ||||||
|  | 		fs.Handler = testHandler | ||||||
|  | 		res, err := fs.ListPhysical(ctx, "/") | ||||||
|  | 		asserts.NoError(err) | ||||||
|  | 		asserts.Len(res, 1) | ||||||
|  | 		asserts.Equal("1", res[0].Name) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
| func TestFileSystem_List(t *testing.T) { | func TestFileSystem_List(t *testing.T) { | ||||||
| 	asserts := assert.New(t) | 	asserts := assert.New(t) | ||||||
| 	fs := &FileSystem{User: &model.User{ | 	fs := &FileSystem{User: &model.User{ | ||||||
|  | |||||||
| @ -27,7 +27,8 @@ type FileHeaderMock struct { | |||||||
| } | } | ||||||
|  |  | ||||||
| func (m FileHeaderMock) List(ctx context.Context, path string, recursive bool) ([]response.Object, error) { | func (m FileHeaderMock) List(ctx context.Context, path string, recursive bool) ([]response.Object, error) { | ||||||
| 	panic("implement me") | 	args := m.Called(ctx, path, recursive) | ||||||
|  | 	return args.Get(0).([]response.Object), args.Error(1) | ||||||
| } | } | ||||||
|  |  | ||||||
| func (m FileHeaderMock) Get(ctx context.Context, path string) (response.RSCloser, error) { | func (m FileHeaderMock) Get(ctx context.Context, path string) (response.RSCloser, error) { | ||||||
|  | |||||||
| @ -413,3 +413,14 @@ func AdminCreateImportTask(c *gin.Context) { | |||||||
| 		c.JSON(200, ErrorResponse(err)) | 		c.JSON(200, ErrorResponse(err)) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // AdminListFolders 列出用户或外部文件系统目录 | ||||||
|  | func AdminListFolders(c *gin.Context) { | ||||||
|  | 	var service admin.ListFolderService | ||||||
|  | 	if err := c.ShouldBindUri(&service); err == nil { | ||||||
|  | 		res := service.List(c) | ||||||
|  | 		c.JSON(200, res) | ||||||
|  | 	} else { | ||||||
|  | 		c.JSON(200, ErrorResponse(err)) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | |||||||
| @ -367,6 +367,9 @@ func InitMasterRouter() *gin.Engine { | |||||||
| 					file.GET("preview/:id", controllers.AdminGetFile) | 					file.GET("preview/:id", controllers.AdminGetFile) | ||||||
| 					// 删除 | 					// 删除 | ||||||
| 					file.POST("delete", controllers.AdminDeleteFile) | 					file.POST("delete", controllers.AdminDeleteFile) | ||||||
|  | 					// 列出用户或外部文件系统目录 | ||||||
|  | 					file.GET("folders/:type/:id/*path", | ||||||
|  | 						controllers.AdminListFolders) | ||||||
| 				} | 				} | ||||||
|  |  | ||||||
| 				share := admin.Group("share") | 				share := admin.Group("share") | ||||||
|  | |||||||
| @ -22,6 +22,71 @@ type FileBatchService struct { | |||||||
| 	Force bool   `json:"force"` | 	Force bool   `json:"force"` | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // ListFolderService 列目录结构 | ||||||
|  | type ListFolderService struct { | ||||||
|  | 	Path string `uri:"path" binding:"required,max=65535"` | ||||||
|  | 	ID   uint   `uri:"id" binding:"required"` | ||||||
|  | 	Type string `uri:"type" binding:"eq=policy|eq=user"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // List 列出指定路径下的目录 | ||||||
|  | func (service *ListFolderService) List(c *gin.Context) serializer.Response { | ||||||
|  | 	if service.Type == "policy" { | ||||||
|  | 		// 列取存储策略中的目录 | ||||||
|  | 		policy, err := model.GetPolicyByID(service.ID) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return serializer.Err(serializer.CodeNotFound, "存储策略不存在", err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// 创建文件系统 | ||||||
|  | 		fs, err := filesystem.NewAnonymousFileSystem() | ||||||
|  | 		if err != nil { | ||||||
|  | 			return serializer.Err(serializer.CodeInternalSetting, "无法创建文件系统", err) | ||||||
|  | 		} | ||||||
|  | 		defer fs.Recycle() | ||||||
|  |  | ||||||
|  | 		// 列取存储策略中的文件 | ||||||
|  | 		fs.Policy = &policy | ||||||
|  | 		res, err := fs.ListPhysical(c.Request.Context(), service.Path) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return serializer.Err(serializer.CodeIOFailed, "无法列取目录", err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		return serializer.Response{ | ||||||
|  | 			Data: map[string]interface{}{ | ||||||
|  | 				"objects": res, | ||||||
|  | 			}, | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 列取用户空间目录 | ||||||
|  | 	// 查找用户 | ||||||
|  | 	user, err := model.GetUserByID(service.ID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return serializer.Err(serializer.CodeNotFound, "用户不存在", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 创建文件系统 | ||||||
|  | 	fs, err := filesystem.NewFileSystem(&user) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return serializer.Err(serializer.CodeInternalSetting, "无法创建文件系统", err) | ||||||
|  | 	} | ||||||
|  | 	defer fs.Recycle() | ||||||
|  |  | ||||||
|  | 	// 列取目录 | ||||||
|  | 	res, err := fs.List(c.Request.Context(), service.Path, nil) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return serializer.Err(serializer.CodeIOFailed, "无法列取目录", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return serializer.Response{ | ||||||
|  | 		Data: map[string]interface{}{ | ||||||
|  | 			"objects": res, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
| // Delete 删除文件 | // Delete 删除文件 | ||||||
| func (service *FileBatchService) Delete(c *gin.Context) serializer.Response { | func (service *FileBatchService) Delete(c *gin.Context) serializer.Response { | ||||||
| 	files, err := model.GetFilesByIDs(service.ID, 0) | 	files, err := model.GetFilesByIDs(service.ID, 0) | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	 HFO4
					HFO4