diff --git a/api/files/files.pb.go b/api/files/files.pb.go
index c5e7beb..ec878cd 100644
--- a/api/files/files.pb.go
+++ b/api/files/files.pb.go
@@ -26,7 +26,7 @@ type FileListReq struct {
 	unknownFields protoimpl.UnknownFields
 
 	Path          string   `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"`                   // 目标文件夹路径
-	UserSpacePath string   `protobuf:"bytes,2,opt,name=UserSpacePath,proto3" json:"UserSpacePath,omitempty"` // 用户空间的路径
+	UserSpacePath string   `protobuf:"bytes,2,opt,name=userSpacePath,proto3" json:"userSpacePath,omitempty"` // 用户空间的路径
 	Sorting       *Sorting `protobuf:"bytes,3,opt,name=sorting,proto3" json:"sorting,omitempty"`
 }
 
@@ -1544,6 +1544,124 @@ func (*ActionResp) Descriptor() ([]byte, []int) {
 	return file_files_proto_rawDescGZIP(), []int{23}
 }
 
+type DirDownloadReq struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Path          string   `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"`
+	UserSpacePath string   `protobuf:"bytes,2,opt,name=userSpacePath,proto3" json:"userSpacePath,omitempty"`
+	Files         []string `protobuf:"bytes,3,rep,name=files,proto3" json:"files,omitempty"`
+	Algo          string   `protobuf:"bytes,4,opt,name=algo,proto3" json:"algo,omitempty"`
+}
+
+func (x *DirDownloadReq) Reset() {
+	*x = DirDownloadReq{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_files_proto_msgTypes[24]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *DirDownloadReq) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*DirDownloadReq) ProtoMessage() {}
+
+func (x *DirDownloadReq) ProtoReflect() protoreflect.Message {
+	mi := &file_files_proto_msgTypes[24]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use DirDownloadReq.ProtoReflect.Descriptor instead.
+func (*DirDownloadReq) Descriptor() ([]byte, []int) {
+	return file_files_proto_rawDescGZIP(), []int{24}
+}
+
+func (x *DirDownloadReq) GetPath() string {
+	if x != nil {
+		return x.Path
+	}
+	return ""
+}
+
+func (x *DirDownloadReq) GetUserSpacePath() string {
+	if x != nil {
+		return x.UserSpacePath
+	}
+	return ""
+}
+
+func (x *DirDownloadReq) GetFiles() []string {
+	if x != nil {
+		return x.Files
+	}
+	return nil
+}
+
+func (x *DirDownloadReq) GetAlgo() string {
+	if x != nil {
+		return x.Algo
+	}
+	return ""
+}
+
+type DirDownloadResp struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Content []byte `protobuf:"bytes,1,opt,name=content,proto3" json:"content,omitempty"`
+}
+
+func (x *DirDownloadResp) Reset() {
+	*x = DirDownloadResp{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_files_proto_msgTypes[25]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *DirDownloadResp) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*DirDownloadResp) ProtoMessage() {}
+
+func (x *DirDownloadResp) ProtoReflect() protoreflect.Message {
+	mi := &file_files_proto_msgTypes[25]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use DirDownloadResp.ProtoReflect.Descriptor instead.
+func (*DirDownloadResp) Descriptor() ([]byte, []int) {
+	return file_files_proto_rawDescGZIP(), []int{25}
+}
+
+func (x *DirDownloadResp) GetContent() []byte {
+	if x != nil {
+		return x.Content
+	}
+	return nil
+}
+
 type SearchResp_Nested struct {
 	state         protoimpl.MessageState
 	sizeCache     protoimpl.SizeCache
@@ -1556,7 +1674,7 @@ type SearchResp_Nested struct {
 func (x *SearchResp_Nested) Reset() {
 	*x = SearchResp_Nested{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_files_proto_msgTypes[24]
+		mi := &file_files_proto_msgTypes[26]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -1569,7 +1687,7 @@ func (x *SearchResp_Nested) String() string {
 func (*SearchResp_Nested) ProtoMessage() {}
 
 func (x *SearchResp_Nested) ProtoReflect() protoreflect.Message {
-	mi := &file_files_proto_msgTypes[24]
+	mi := &file_files_proto_msgTypes[26]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -1605,9 +1723,9 @@ var file_files_proto_rawDesc = []byte{
 	0x0a, 0x0b, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x66,
 	0x69, 0x6c, 0x65, 0x73, 0x22, 0x71, 0x0a, 0x0b, 0x46, 0x69, 0x6c, 0x65, 0x4c, 0x69, 0x73, 0x74,
 	0x52, 0x65, 0x71, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28,
-	0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x24, 0x0a, 0x0d, 0x55, 0x73, 0x65, 0x72, 0x53,
+	0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x24, 0x0a, 0x0d, 0x75, 0x73, 0x65, 0x72, 0x53,
 	0x70, 0x61, 0x63, 0x65, 0x50, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d,
-	0x55, 0x73, 0x65, 0x72, 0x53, 0x70, 0x61, 0x63, 0x65, 0x50, 0x61, 0x74, 0x68, 0x12, 0x28, 0x0a,
+	0x75, 0x73, 0x65, 0x72, 0x53, 0x70, 0x61, 0x63, 0x65, 0x50, 0x61, 0x74, 0x68, 0x12, 0x28, 0x0a,
 	0x07, 0x73, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e,
 	0x2e, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x2e, 0x53, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x52, 0x07,
 	0x73, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x22, 0xd9, 0x01, 0x0a, 0x05, 0x49, 0x74, 0x65, 0x6d,
@@ -1754,46 +1872,60 @@ var file_files_proto_rawDesc = []byte{
 	0x01, 0x28, 0x08, 0x52, 0x08, 0x6f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x12, 0x16, 0x0a,
 	0x06, 0x72, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x72,
 	0x65, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x0c, 0x0a, 0x0a, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52,
-	0x65, 0x73, 0x70, 0x32, 0xdb, 0x04, 0x0a, 0x04, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x31, 0x0a, 0x04,
-	0x4c, 0x69, 0x73, 0x74, 0x12, 0x12, 0x2e, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x2e, 0x46, 0x69, 0x6c,
-	0x65, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x1a, 0x13, 0x2e, 0x66, 0x69, 0x6c, 0x65, 0x73,
-	0x2e, 0x46, 0x69, 0x6c, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12,
-	0x31, 0x0a, 0x04, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x12, 0x2e, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x2e,
-	0x46, 0x69, 0x6c, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x1a, 0x13, 0x2e, 0x66, 0x69,
-	0x6c, 0x65, 0x73, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70,
-	0x22, 0x00, 0x12, 0x2f, 0x0a, 0x06, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x10, 0x2e, 0x66,
-	0x69, 0x6c, 0x65, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x1a, 0x11,
-	0x2e, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73,
-	0x70, 0x22, 0x00, 0x12, 0x2f, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x10, 0x2e,
-	0x66, 0x69, 0x6c, 0x65, 0x73, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x1a,
-	0x11, 0x2e, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65,
-	0x73, 0x70, 0x22, 0x00, 0x12, 0x2f, 0x0a, 0x06, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x12, 0x10,
-	0x2e, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71,
-	0x1a, 0x11, 0x2e, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52,
-	0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x2f, 0x0a, 0x06, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x12,
-	0x10, 0x2e, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x2e, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65,
-	0x71, 0x1a, 0x11, 0x2e, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x2e, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64,
-	0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x38, 0x0a, 0x09, 0x54, 0x75, 0x73, 0x43, 0x72, 0x65,
-	0x61, 0x74, 0x65, 0x12, 0x13, 0x2e, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x2e, 0x54, 0x75, 0x73, 0x43,
-	0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x1a, 0x14, 0x2e, 0x66, 0x69, 0x6c, 0x65, 0x73,
-	0x2e, 0x54, 0x75, 0x73, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00,
-	0x12, 0x38, 0x0a, 0x09, 0x54, 0x75, 0x73, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x13, 0x2e,
-	0x66, 0x69, 0x6c, 0x65, 0x73, 0x2e, 0x54, 0x75, 0x73, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x52,
-	0x65, 0x71, 0x1a, 0x14, 0x2e, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x2e, 0x54, 0x75, 0x73, 0x55, 0x70,
-	0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x50, 0x0a, 0x11, 0x52, 0x65,
-	0x73, 0x75, 0x6d, 0x61, 0x62, 0x6c, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x12,
-	0x1b, 0x2e, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x2e, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x61, 0x62, 0x6c,
-	0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x52, 0x65, 0x71, 0x1a, 0x1c, 0x2e, 0x66,
-	0x69, 0x6c, 0x65, 0x73, 0x2e, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x61, 0x62, 0x6c, 0x65, 0x54, 0x72,
-	0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x32, 0x0a, 0x07,
-	0x50, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x12, 0x11, 0x2e, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x2e,
-	0x50, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x52, 0x65, 0x71, 0x1a, 0x12, 0x2e, 0x66, 0x69, 0x6c,
-	0x65, 0x73, 0x2e, 0x50, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00,
-	0x12, 0x2f, 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x10, 0x2e, 0x66, 0x69, 0x6c,
-	0x65, 0x73, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x1a, 0x11, 0x2e, 0x66,
-	0x69, 0x6c, 0x65, 0x73, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x22,
-	0x00, 0x42, 0x0a, 0x5a, 0x08, 0x2e, 0x2f, 0x3b, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x62, 0x06, 0x70,
-	0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x65, 0x73, 0x70, 0x22, 0x74, 0x0a, 0x0e, 0x44, 0x69, 0x72, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f,
+	0x61, 0x64, 0x52, 0x65, 0x71, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20,
+	0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x24, 0x0a, 0x0d, 0x75, 0x73, 0x65,
+	0x72, 0x53, 0x70, 0x61, 0x63, 0x65, 0x50, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
+	0x52, 0x0d, 0x75, 0x73, 0x65, 0x72, 0x53, 0x70, 0x61, 0x63, 0x65, 0x50, 0x61, 0x74, 0x68, 0x12,
+	0x14, 0x0a, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05,
+	0x66, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x61, 0x6c, 0x67, 0x6f, 0x18, 0x04, 0x20,
+	0x01, 0x28, 0x09, 0x52, 0x04, 0x61, 0x6c, 0x67, 0x6f, 0x22, 0x2b, 0x0a, 0x0f, 0x44, 0x69, 0x72,
+	0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x12, 0x18, 0x0a, 0x07,
+	0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x63,
+	0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x32, 0x9d, 0x05, 0x0a, 0x04, 0x46, 0x69, 0x6c, 0x65, 0x12,
+	0x31, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x12, 0x2e, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x2e,
+	0x46, 0x69, 0x6c, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x1a, 0x13, 0x2e, 0x66, 0x69,
+	0x6c, 0x65, 0x73, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70,
+	0x22, 0x00, 0x12, 0x31, 0x0a, 0x04, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x12, 0x2e, 0x66, 0x69, 0x6c,
+	0x65, 0x73, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x1a, 0x13,
+	0x2e, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52,
+	0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x2f, 0x0a, 0x06, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12,
+	0x10, 0x2e, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65,
+	0x71, 0x1a, 0x11, 0x2e, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65,
+	0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x2f, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65,
+	0x12, 0x10, 0x2e, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52,
+	0x65, 0x71, 0x1a, 0x11, 0x2e, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74,
+	0x65, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x2f, 0x0a, 0x06, 0x53, 0x65, 0x61, 0x72, 0x63,
+	0x68, 0x12, 0x10, 0x2e, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68,
+	0x52, 0x65, 0x71, 0x1a, 0x11, 0x2e, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72,
+	0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x2f, 0x0a, 0x06, 0x55, 0x70, 0x6c, 0x6f,
+	0x61, 0x64, 0x12, 0x10, 0x2e, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x2e, 0x55, 0x70, 0x6c, 0x6f, 0x61,
+	0x64, 0x52, 0x65, 0x71, 0x1a, 0x11, 0x2e, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x2e, 0x55, 0x70, 0x6c,
+	0x6f, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x38, 0x0a, 0x09, 0x54, 0x75, 0x73,
+	0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x13, 0x2e, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x2e, 0x54,
+	0x75, 0x73, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x1a, 0x14, 0x2e, 0x66, 0x69,
+	0x6c, 0x65, 0x73, 0x2e, 0x54, 0x75, 0x73, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73,
+	0x70, 0x22, 0x00, 0x12, 0x38, 0x0a, 0x09, 0x54, 0x75, 0x73, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64,
+	0x12, 0x13, 0x2e, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x2e, 0x54, 0x75, 0x73, 0x55, 0x70, 0x6c, 0x6f,
+	0x61, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x14, 0x2e, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x2e, 0x54, 0x75,
+	0x73, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x50, 0x0a,
+	0x11, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x61, 0x62, 0x6c, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66,
+	0x65, 0x72, 0x12, 0x1b, 0x2e, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x2e, 0x52, 0x65, 0x73, 0x75, 0x6d,
+	0x61, 0x62, 0x6c, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x52, 0x65, 0x71, 0x1a,
+	0x1c, 0x2e, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x2e, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x61, 0x62, 0x6c,
+	0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12,
+	0x32, 0x0a, 0x07, 0x50, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x12, 0x11, 0x2e, 0x66, 0x69, 0x6c,
+	0x65, 0x73, 0x2e, 0x50, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x52, 0x65, 0x71, 0x1a, 0x12, 0x2e,
+	0x66, 0x69, 0x6c, 0x65, 0x73, 0x2e, 0x50, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x52, 0x65, 0x73,
+	0x70, 0x22, 0x00, 0x12, 0x2f, 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x10, 0x2e,
+	0x66, 0x69, 0x6c, 0x65, 0x73, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x1a,
+	0x11, 0x2e, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65,
+	0x73, 0x70, 0x22, 0x00, 0x12, 0x40, 0x0a, 0x0b, 0x44, 0x69, 0x72, 0x44, 0x6f, 0x77, 0x6e, 0x6c,
+	0x6f, 0x61, 0x64, 0x12, 0x15, 0x2e, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x2e, 0x44, 0x69, 0x72, 0x44,
+	0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x16, 0x2e, 0x66, 0x69, 0x6c,
+	0x65, 0x73, 0x2e, 0x44, 0x69, 0x72, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65,
+	0x73, 0x70, 0x22, 0x00, 0x30, 0x01, 0x42, 0x0a, 0x5a, 0x08, 0x2e, 0x2f, 0x3b, 0x66, 0x69, 0x6c,
+	0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
@@ -1808,7 +1940,7 @@ func file_files_proto_rawDescGZIP() []byte {
 	return file_files_proto_rawDescData
 }
 
-var file_files_proto_msgTypes = make([]protoimpl.MessageInfo, 25)
+var file_files_proto_msgTypes = make([]protoimpl.MessageInfo, 27)
 var file_files_proto_goTypes = []interface{}{
 	(*FileListReq)(nil),           // 0: files.FileListReq
 	(*Items)(nil),                 // 1: files.Items
@@ -1834,13 +1966,15 @@ var file_files_proto_goTypes = []interface{}{
 	(*PreviewResp)(nil),           // 21: files.PreviewResp
 	(*ActionReq)(nil),             // 22: files.ActionReq
 	(*ActionResp)(nil),            // 23: files.ActionResp
-	(*SearchResp_Nested)(nil),     // 24: files.searchResp.Nested
+	(*DirDownloadReq)(nil),        // 24: files.DirDownloadReq
+	(*DirDownloadResp)(nil),       // 25: files.DirDownloadResp
+	(*SearchResp_Nested)(nil),     // 26: files.searchResp.Nested
 }
 var file_files_proto_depIdxs = []int32{
 	2,  // 0: files.FileListReq.sorting:type_name -> files.Sorting
 	1,  // 1: files.FileListResp.items:type_name -> files.Items
 	2,  // 2: files.FileListResp.sorting:type_name -> files.Sorting
-	24, // 3: files.searchResp.items:type_name -> files.searchResp.Nested
+	26, // 3: files.searchResp.items:type_name -> files.searchResp.Nested
 	0,  // 4: files.File.List:input_type -> files.FileListReq
 	18, // 5: files.File.Info:input_type -> files.FileInfoReq
 	4,  // 6: files.File.Create:input_type -> files.CreateReq
@@ -1852,19 +1986,21 @@ var file_files_proto_depIdxs = []int32{
 	16, // 12: files.File.ResumableTransfer:input_type -> files.ResumableTransferReq
 	20, // 13: files.File.Preview:input_type -> files.PreviewReq
 	22, // 14: files.File.Action:input_type -> files.ActionReq
-	3,  // 15: files.File.List:output_type -> files.FileListResp
-	19, // 16: files.File.Info:output_type -> files.FileInfoResp
-	5,  // 17: files.File.Create:output_type -> files.CreateResp
-	7,  // 18: files.File.Delete:output_type -> files.DeleteResp
-	11, // 19: files.File.Search:output_type -> files.searchResp
-	9,  // 20: files.File.Upload:output_type -> files.UploadResp
-	13, // 21: files.File.TusCreate:output_type -> files.TusCreateResp
-	15, // 22: files.File.TusUpload:output_type -> files.TusUploadResp
-	17, // 23: files.File.ResumableTransfer:output_type -> files.ResumableTransferResp
-	21, // 24: files.File.Preview:output_type -> files.PreviewResp
-	23, // 25: files.File.Action:output_type -> files.ActionResp
-	15, // [15:26] is the sub-list for method output_type
-	4,  // [4:15] is the sub-list for method input_type
+	24, // 15: files.File.DirDownload:input_type -> files.DirDownloadReq
+	3,  // 16: files.File.List:output_type -> files.FileListResp
+	19, // 17: files.File.Info:output_type -> files.FileInfoResp
+	5,  // 18: files.File.Create:output_type -> files.CreateResp
+	7,  // 19: files.File.Delete:output_type -> files.DeleteResp
+	11, // 20: files.File.Search:output_type -> files.searchResp
+	9,  // 21: files.File.Upload:output_type -> files.UploadResp
+	13, // 22: files.File.TusCreate:output_type -> files.TusCreateResp
+	15, // 23: files.File.TusUpload:output_type -> files.TusUploadResp
+	17, // 24: files.File.ResumableTransfer:output_type -> files.ResumableTransferResp
+	21, // 25: files.File.Preview:output_type -> files.PreviewResp
+	23, // 26: files.File.Action:output_type -> files.ActionResp
+	25, // 27: files.File.DirDownload:output_type -> files.DirDownloadResp
+	16, // [16:28] is the sub-list for method output_type
+	4,  // [4:16] is the sub-list for method input_type
 	4,  // [4:4] is the sub-list for extension type_name
 	4,  // [4:4] is the sub-list for extension extendee
 	0,  // [0:4] is the sub-list for field type_name
@@ -2165,6 +2301,30 @@ func file_files_proto_init() {
 			}
 		}
 		file_files_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*DirDownloadReq); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_files_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*DirDownloadResp); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_files_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*SearchResp_Nested); i {
 			case 0:
 				return &v.state
@@ -2183,7 +2343,7 @@ func file_files_proto_init() {
 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
 			RawDescriptor: file_files_proto_rawDesc,
 			NumEnums:      0,
-			NumMessages:   25,
+			NumMessages:   27,
 			NumExtensions: 0,
 			NumServices:   1,
 		},
diff --git a/api/files/files.proto b/api/files/files.proto
index 1c30830..94c7b9d 100644
--- a/api/files/files.proto
+++ b/api/files/files.proto
@@ -16,6 +16,7 @@ service File{
     rpc ResumableTransfer(ResumableTransferReq) returns (ResumableTransferResp) {} // 断点续传的grpc实现
     rpc Preview(PreviewReq) returns (PreviewResp) {} // 文件预览
     rpc Action(ActionReq) returns (ActionResp) {} // 移动文件或重命名文件
+    rpc DirDownload(DirDownloadReq) returns (stream DirDownloadResp) {} // 文件夹压缩下载
 } 
 
 message FileListReq{
@@ -174,6 +175,17 @@ message ActionReq{
 }
 
 message ActionResp{
+
+}
+
+message DirDownloadReq {
+    string path = 1;
+    string userSpacePath = 2;
+    repeated string files =3;
+    string algo = 4;
     
 }
 
+message DirDownloadResp {
+    bytes content = 1;
+}
diff --git a/api/files/files.validator.pb.go b/api/files/files.validator.pb.go
index 350ceae..7a70050 100644
--- a/api/files/files.validator.pb.go
+++ b/api/files/files.validator.pb.go
@@ -114,3 +114,9 @@ func (this *ActionReq) Validate() error {
 func (this *ActionResp) Validate() error {
 	return nil
 }
+func (this *DirDownloadReq) Validate() error {
+	return nil
+}
+func (this *DirDownloadResp) Validate() error {
+	return nil
+}
diff --git a/api/files/files_triple.pb.go b/api/files/files_triple.pb.go
index 9f8a95f..665002e 100644
--- a/api/files/files_triple.pb.go
+++ b/api/files/files_triple.pb.go
@@ -8,9 +8,11 @@ package files
 
 import (
 	context "context"
+	constant1 "dubbo.apache.org/dubbo-go/v3/common/constant"
 	protocol "dubbo.apache.org/dubbo-go/v3/protocol"
 	dubbo3 "dubbo.apache.org/dubbo-go/v3/protocol/dubbo3"
 	invocation "dubbo.apache.org/dubbo-go/v3/protocol/invocation"
+	fmt "fmt"
 	grpc_go "github.com/dubbogo/grpc-go"
 	codes "github.com/dubbogo/grpc-go/codes"
 	metadata "github.com/dubbogo/grpc-go/metadata"
@@ -39,6 +41,7 @@ type FileClient interface {
 	ResumableTransfer(ctx context.Context, in *ResumableTransferReq, opts ...grpc_go.CallOption) (*ResumableTransferResp, common.ErrorWithAttachment)
 	Preview(ctx context.Context, in *PreviewReq, opts ...grpc_go.CallOption) (*PreviewResp, common.ErrorWithAttachment)
 	Action(ctx context.Context, in *ActionReq, opts ...grpc_go.CallOption) (*ActionResp, common.ErrorWithAttachment)
+	DirDownload(ctx context.Context, in *DirDownloadReq, opts ...grpc_go.CallOption) (File_DirDownloadClient, error)
 }
 
 type fileClient struct {
@@ -57,6 +60,7 @@ type FileClientImpl struct {
 	ResumableTransfer func(ctx context.Context, in *ResumableTransferReq) (*ResumableTransferResp, error)
 	Preview           func(ctx context.Context, in *PreviewReq) (*PreviewResp, error)
 	Action            func(ctx context.Context, in *ActionReq) (*ActionResp, error)
+	DirDownload       func(ctx context.Context, in *DirDownloadReq) (File_DirDownloadClient, error)
 }
 
 func (c *FileClientImpl) GetDubboStub(cc *triple.TripleConn) FileClient {
@@ -137,6 +141,39 @@ func (c *fileClient) Action(ctx context.Context, in *ActionReq, opts ...grpc_go.
 	return out, c.cc.Invoke(ctx, "/"+interfaceKey+"/Action", in, out)
 }
 
+func (c *fileClient) DirDownload(ctx context.Context, in *DirDownloadReq, opts ...grpc_go.CallOption) (File_DirDownloadClient, error) {
+	interfaceKey := ctx.Value(constant.InterfaceKey).(string)
+	stream, err := c.cc.NewStream(ctx, "/"+interfaceKey+"/DirDownload", opts...)
+	if err != nil {
+		return nil, err
+	}
+	x := &fileDirDownloadClient{stream}
+	if err := x.ClientStream.SendMsg(in); err != nil {
+		return nil, err
+	}
+	if err := x.ClientStream.CloseSend(); err != nil {
+		return nil, err
+	}
+	return x, nil
+}
+
+type File_DirDownloadClient interface {
+	Recv() (*DirDownloadResp, error)
+	grpc_go.ClientStream
+}
+
+type fileDirDownloadClient struct {
+	grpc_go.ClientStream
+}
+
+func (x *fileDirDownloadClient) Recv() (*DirDownloadResp, error) {
+	m := new(DirDownloadResp)
+	if err := x.ClientStream.RecvMsg(m); err != nil {
+		return nil, err
+	}
+	return m, nil
+}
+
 // FileServer is the server API for File service.
 // All implementations must embed UnimplementedFileServer
 // for forward compatibility
@@ -152,6 +189,7 @@ type FileServer interface {
 	ResumableTransfer(context.Context, *ResumableTransferReq) (*ResumableTransferResp, error)
 	Preview(context.Context, *PreviewReq) (*PreviewResp, error)
 	Action(context.Context, *ActionReq) (*ActionResp, error)
+	DirDownload(*DirDownloadReq, File_DirDownloadServer) error
 	mustEmbedUnimplementedFileServer()
 }
 
@@ -193,6 +231,9 @@ func (UnimplementedFileServer) Preview(context.Context, *PreviewReq) (*PreviewRe
 func (UnimplementedFileServer) Action(context.Context, *ActionReq) (*ActionResp, error) {
 	return nil, status.Errorf(codes.Unimplemented, "method Action not implemented")
 }
+func (UnimplementedFileServer) DirDownload(*DirDownloadReq, File_DirDownloadServer) error {
+	return status.Errorf(codes.Unimplemented, "method DirDownload not implemented")
+}
 func (s *UnimplementedFileServer) XXX_SetProxyImpl(impl protocol.Invoker) {
 	s.proxyImpl = impl
 }
@@ -540,6 +581,40 @@ func _File_Action_Handler(srv interface{}, ctx context.Context, dec func(interfa
 	return interceptor(ctx, in, info, handler)
 }
 
+func _File_DirDownload_Handler(srv interface{}, stream grpc_go.ServerStream) error {
+	_, ok := srv.(dubbo3.Dubbo3GrpcService)
+	ctx := stream.Context()
+	md, _ := metadata.FromIncomingContext(ctx)
+	invAttachment := make(map[string]interface{}, len(md))
+	for k, v := range md {
+		invAttachment[k] = v
+	}
+	stream.(grpc_go.CtxSetterStream).SetContext(context.WithValue(ctx, constant1.AttachmentKey, invAttachment))
+	invo := invocation.NewRPCInvocation("DirDownload", nil, nil)
+	if !ok {
+		fmt.Println(invo)
+		return nil
+	}
+	m := new(DirDownloadReq)
+	if err := stream.RecvMsg(m); err != nil {
+		return err
+	}
+	return srv.(FileServer).DirDownload(m, &fileDirDownloadServer{stream})
+}
+
+type File_DirDownloadServer interface {
+	Send(*DirDownloadResp) error
+	grpc_go.ServerStream
+}
+
+type fileDirDownloadServer struct {
+	grpc_go.ServerStream
+}
+
+func (x *fileDirDownloadServer) Send(m *DirDownloadResp) error {
+	return x.ServerStream.SendMsg(m)
+}
+
 // File_ServiceDesc is the grpc_go.ServiceDesc for File service.
 // It's only intended for direct use with grpc_go.RegisterService,
 // and not to be introspected or modified (even as a copy)
@@ -592,6 +667,12 @@ var File_ServiceDesc = grpc_go.ServiceDesc{
 			Handler:    _File_Action_Handler,
 		},
 	},
-	Streams:  []grpc_go.StreamDesc{},
+	Streams: []grpc_go.StreamDesc{
+		{
+			StreamName:    "DirDownload",
+			Handler:       _File_DirDownload_Handler,
+			ServerStreams: true,
+		},
+	},
 	Metadata: "files.proto",
 }
diff --git a/go.mod b/go.mod
index 3dc5b6e..92e790f 100644
--- a/go.mod
+++ b/go.mod
@@ -27,7 +27,7 @@ require (
 	github.com/spf13/cobra v1.8.1
 	github.com/spf13/pflag v1.0.5
 	github.com/spf13/viper v1.19.0
-	github.com/stretchr/testify v1.9.0
+	github.com/stretchr/testify v1.10.0
 	github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce
 	go.etcd.io/bbolt v1.3.11
 	golang.org/x/crypto v0.36.0
@@ -134,6 +134,7 @@ require (
 	github.com/robfig/cron/v3 v3.0.1 // indirect
 	github.com/sagikazarmark/locafero v0.4.0 // indirect
 	github.com/sagikazarmark/slog-shim v0.1.0 // indirect
+	github.com/samber/lo v1.50.0
 	github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b // indirect
 	github.com/shirou/gopsutil v3.20.11+incompatible // indirect
 	github.com/shoenig/go-m1cpu v0.1.6 // indirect
diff --git a/go.sum b/go.sum
index e581c05..71f5277 100644
--- a/go.sum
+++ b/go.sum
@@ -787,6 +787,8 @@ github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6ke
 github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
 github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
 github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
+github.com/samber/lo v1.50.0 h1:XrG0xOeHs+4FQ8gJR97zDz5uOFMW7OwFWiFVzqopKgY=
+github.com/samber/lo v1.50.0/go.mod h1:RjZyNk6WSnUFRKK6EyOhsRJMqft3G+pg7dCWHQCWvsc=
 github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
 github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b h1:gQZ0qzfKHQIybLANtM3mBXNUtOfsCFXeTsnBqCsx1KM=
 github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
@@ -860,6 +862,7 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
 github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
diff --git a/service/files.go b/service/files.go
index 8ff2f33..1c0cf0b 100644
--- a/service/files.go
+++ b/service/files.go
@@ -6,12 +6,14 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"log"
 	"os"
 	"path"
 	"path/filepath"
 	"strings"
 
 	"dubbo.apache.org/dubbo-go/v3/common/logger"
+	"github.com/dubbogo/grpc-go/metadata"
 	filesApi "github.com/filebrowser/filebrowser/v2/api/files"
 	fbErrors "github.com/filebrowser/filebrowser/v2/errors"
 	"github.com/filebrowser/filebrowser/v2/files"
@@ -20,6 +22,8 @@ import (
 	"github.com/filebrowser/filebrowser/v2/img"
 	"github.com/filebrowser/filebrowser/v2/rules"
 	"github.com/filebrowser/filebrowser/v2/search"
+	"github.com/mholt/archiver/v3"
+	"github.com/samber/lo"
 	"github.com/spf13/afero"
 )
 
@@ -310,11 +314,11 @@ func (f *FilesProvider) Preview(ctx context.Context, req *filesApi.PreviewReq) (
 
 		}
 	default:
-		return nil, fmt.Errorf("目前只支持image类型的预览")
+		return nil, errors.New("目前只支持图片类型的预览")
 	}
 }
 
-func ( *FilesProvider) createPreview(imgSvc http.ImgService,
+func (f *FilesProvider) createPreview(imgSvc http.ImgService,
 	file *files.FileInfo, previewSize uint32) ([]byte, error) {
 	fd, err := file.Fs.Open(file.Path)
 	if err != nil {
@@ -368,6 +372,86 @@ func (f *FilesProvider) Action(_ context.Context, req *filesApi.ActionReq) (*fil
 	return &filesApi.ActionResp{}, patchAction(req.Action, req.Path, req.Destination, fs)
 }
 
+func (f *FilesProvider) DirDownload(req *filesApi.DirDownloadReq, stream filesApi.File_DirDownloadServer) error {
+	var filenames []string
+	writer := newFileDownloadWriter(stream)
+	fs := getFs(req.UserSpacePath)
+	file, err := files.NewFileInfo(&files.FileOptions{
+		Fs:         fs,
+		Path:       req.Path,
+		Modify:     true,
+		Expand:     false,
+		ReadHeader: true,
+		Checker:    rules.EmptyChecker,
+	})
+	if err != nil {
+		return err
+	}
+
+	if len(req.Files) == 0 {
+		filenames = append(filenames, file.Path)
+	} else {
+		filenames = lo.Map(req.Files, func(name string, _ int) string {
+			return filepath.Join(req.Path, slashClean(name))
+		})
+	}
+
+	extension, ar, err := parseQueryAlgorithm(req.Algo)
+	if err != nil {
+		return err
+	}
+
+	err = ar.Create(writer)
+	if err != nil {
+		return err
+	}
+	defer ar.Close()
+
+	commonDir := fileutils.CommonPrefix(filepath.Separator, filenames...)
+
+	name := filepath.Base(commonDir)
+	if name == "." || name == "" || name == string(filepath.Separator) {
+		name = file.Name
+	}
+	// Prefix used to distinguish a filelist generated
+	// archive from the full directory archive
+	if len(filenames) > 1 {
+		name = "_" + name
+	}
+	name += extension
+	stream.SendHeader(metadata.MD{
+		"filename": []string{name},
+	})
+	for _, fname := range filenames {
+		err = addFile(ar, fs, fname, commonDir)
+		if err != nil {
+			log.Printf("Failed to archive %s: %v", fname, err)
+		}
+	}
+
+	return nil
+}
+
+type fileDownloadWriter struct { // 用于实现archiver.Writer.Create(out io.Writer) error
+	stream filesApi.File_DirDownloadServer
+}
+
+func (f *fileDownloadWriter) Write(p []byte) (n int, err error) {
+	err = f.stream.Send(&filesApi.DirDownloadResp{
+		Content: p,
+	})
+	if err == nil {
+		n = len(p)
+	}
+	return
+}
+
+func newFileDownloadWriter(stream filesApi.File_DirDownloadServer) *fileDownloadWriter {
+	return &fileDownloadWriter{
+		stream: stream,
+	}
+}
+
 func getFs(UserSpacePath string) afero.Fs {
 	bashAbs, _ := filepath.Abs(BASE_PATH)
 	if !strings.HasPrefix(UserSpacePath, "/") {
@@ -433,3 +517,80 @@ func patchAction(action, src, dst string, fs afero.Fs) error {
 		return fmt.Errorf("unsupported action %s: %w", action, fbErrors.ErrInvalidRequestParams)
 	}
 }
+
+func slashClean(name string) string {
+	if name == "" || name[0] != '/' {
+		name = "/" + name
+	}
+	return path.Clean(name)
+}
+
+func parseQueryAlgorithm(algo string) (string, archiver.Writer, error) {
+	switch algo {
+	case "zip", "true", "":
+		return ".zip", archiver.NewZip(), nil
+	case "tar":
+		return ".tar", archiver.NewTar(), nil
+	case "targz":
+		return ".tar.gz", archiver.NewTarGz(), nil
+	case "tarbz2":
+		return ".tar.bz2", archiver.NewTarBz2(), nil
+	case "tarxz":
+		return ".tar.xz", archiver.NewTarXz(), nil
+	case "tarlz4":
+		return ".tar.lz4", archiver.NewTarLz4(), nil
+	case "tarsz":
+		return ".tar.sz", archiver.NewTarSz(), nil
+	default:
+		return "", nil, errors.New("format not implemented")
+	}
+}
+
+func addFile(ar archiver.Writer, fs afero.Fs, path, commonPath string) error {
+	info, err := fs.Stat(path)
+	if err != nil {
+		return err
+	}
+
+	if !info.IsDir() && !info.Mode().IsRegular() {
+		return nil
+	}
+
+	file, err := fs.Open(path)
+	if err != nil {
+		return err
+	}
+	defer file.Close()
+
+	if path != commonPath {
+		filename := strings.TrimPrefix(path, commonPath)
+		filename = strings.TrimPrefix(filename, string(filepath.Separator))
+		err = ar.Write(archiver.File{
+			FileInfo: archiver.FileInfo{
+				FileInfo:   info,
+				CustomName: filename,
+			},
+			ReadCloser: file,
+		})
+		if err != nil {
+			return err
+		}
+	}
+
+	if info.IsDir() {
+		names, err := file.Readdirnames(0)
+		if err != nil {
+			return err
+		}
+
+		for _, name := range names {
+			fPath := filepath.Join(path, name)
+			err = addFile(ar, fs, fPath, commonPath)
+			if err != nil {
+				log.Printf("Failed to archive %s: %v", fPath, err)
+			}
+		}
+	}
+
+	return nil
+}