github.com/qiniu/go-sdk/v7@v7.27.0/sandbox/filesystem.go (about) 1 package sandbox 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "net/http" 8 "time" 9 10 "connectrpc.com/connect" 11 12 "github.com/qiniu/go-sdk/v7/sandbox/internal/envdapi/filesystem" 13 "github.com/qiniu/go-sdk/v7/sandbox/internal/envdapi/filesystem/filesystemconnect" 14 ) 15 16 // FileType 文件类型。 17 type FileType string 18 19 const ( 20 // FileTypeFile 表示普通文件。 21 FileTypeFile FileType = "file" 22 // FileTypeDirectory 表示目录。 23 FileTypeDirectory FileType = "dir" 24 // FileTypeUnknown 表示未知文件类型。 25 // 当 protobuf 返回了 SDK 尚未识别的新文件类型时使用此值。 26 FileTypeUnknown FileType = "unknown" 27 ) 28 29 // EntryInfo 文件或目录的元信息。 30 type EntryInfo struct { 31 Name string 32 Type FileType 33 Path string 34 Size int64 35 Mode uint32 36 Permissions string 37 Owner string 38 Group string 39 ModifiedTime time.Time 40 SymlinkTarget *string 41 } 42 43 // entryInfoFromProto 将 protobuf EntryInfo 转换为 SDK EntryInfo。 44 func entryInfoFromProto(e *filesystem.EntryInfo) *EntryInfo { 45 if e == nil { 46 return nil 47 } 48 info := &EntryInfo{ 49 Name: e.Name, 50 Path: e.Path, 51 Size: e.Size, 52 Mode: e.Mode, 53 Permissions: e.Permissions, 54 Owner: e.Owner, 55 Group: e.Group, 56 } 57 switch e.Type { 58 case filesystem.FileType_FILE_TYPE_FILE: 59 info.Type = FileTypeFile 60 case filesystem.FileType_FILE_TYPE_DIRECTORY: 61 info.Type = FileTypeDirectory 62 default: 63 info.Type = FileTypeUnknown 64 } 65 if e.ModifiedTime != nil { 66 info.ModifiedTime = e.ModifiedTime.AsTime() 67 } 68 if e.SymlinkTarget != nil { 69 t := *e.SymlinkTarget 70 info.SymlinkTarget = &t 71 } 72 return info 73 } 74 75 // EventType 文件系统事件类型。 76 type EventType string 77 78 const ( 79 // EventCreate 文件或目录被创建。 80 EventCreate EventType = "create" 81 // EventWrite 文件被写入。 82 EventWrite EventType = "write" 83 // EventRemove 文件或目录被删除。 84 EventRemove EventType = "remove" 85 // EventRename 文件或目录被重命名。 86 EventRename EventType = "rename" 87 // EventChmod 文件或目录权限被修改。 88 EventChmod EventType = "chmod" 89 ) 90 91 // FilesystemEvent 文件系统事件。 92 type FilesystemEvent struct { 93 Name string 94 Type EventType 95 } 96 97 // filesystemEventFromProto 将 protobuf FilesystemEvent 转换为 SDK FilesystemEvent。 98 func filesystemEventFromProto(e *filesystem.FilesystemEvent) FilesystemEvent { 99 ev := FilesystemEvent{Name: e.Name} 100 switch e.Type { 101 case filesystem.EventType_EVENT_TYPE_CREATE: 102 ev.Type = EventCreate 103 case filesystem.EventType_EVENT_TYPE_WRITE: 104 ev.Type = EventWrite 105 case filesystem.EventType_EVENT_TYPE_REMOVE: 106 ev.Type = EventRemove 107 case filesystem.EventType_EVENT_TYPE_RENAME: 108 ev.Type = EventRename 109 case filesystem.EventType_EVENT_TYPE_CHMOD: 110 ev.Type = EventChmod 111 } 112 return ev 113 } 114 115 // FilesystemOption 文件系统操作选项。 116 type FilesystemOption func(*filesystemOpts) 117 118 type filesystemOpts struct { 119 user string 120 } 121 122 // WithUser 设置文件系统操作的用户身份。 123 func WithUser(user string) FilesystemOption { 124 return func(o *filesystemOpts) { o.user = user } 125 } 126 127 func applyFilesystemOpts(opts []FilesystemOption) *filesystemOpts { 128 o := &filesystemOpts{user: DefaultUser} 129 for _, fn := range opts { 130 fn(o) 131 } 132 return o 133 } 134 135 // ListOption 列目录选项。 136 type ListOption func(*listOpts) 137 138 type listOpts struct { 139 filesystemOpts 140 depth uint32 141 } 142 143 // WithDepth 设置列目录的递归深度,默认为 1。 144 func WithDepth(depth uint32) ListOption { 145 return func(o *listOpts) { o.depth = depth } 146 } 147 148 // WithListUser 设置列目录操作的用户身份。 149 func WithListUser(user string) ListOption { 150 return func(o *listOpts) { o.user = user } 151 } 152 153 func applyListOpts(opts []ListOption) *listOpts { 154 o := &listOpts{ 155 filesystemOpts: filesystemOpts{user: DefaultUser}, 156 depth: 1, 157 } 158 for _, fn := range opts { 159 fn(o) 160 } 161 return o 162 } 163 164 // WatchOption 目录监听选项。 165 type WatchOption func(*watchOpts) 166 167 type watchOpts struct { 168 filesystemOpts 169 recursive bool 170 } 171 172 // WithRecursive 设置是否递归监听子目录。 173 func WithRecursive(recursive bool) WatchOption { 174 return func(o *watchOpts) { o.recursive = recursive } 175 } 176 177 // WithWatchUser 设置监听操作的用户身份。 178 func WithWatchUser(user string) WatchOption { 179 return func(o *watchOpts) { o.user = user } 180 } 181 182 func applyWatchOpts(opts []WatchOption) *watchOpts { 183 o := &watchOpts{ 184 filesystemOpts: filesystemOpts{user: DefaultUser}, 185 } 186 for _, fn := range opts { 187 fn(o) 188 } 189 return o 190 } 191 192 // WatchHandle 目录监听句柄。 193 type WatchHandle struct { 194 events chan FilesystemEvent 195 cancel context.CancelFunc 196 done chan struct{} 197 err error 198 } 199 200 // Events 返回文件系统事件通道。 201 func (w *WatchHandle) Events() <-chan FilesystemEvent { 202 return w.events 203 } 204 205 // Err 返回监听过程中发生的错误。应在 Events 通道关闭后调用。 206 // 若通过 Stop() 正常停止,Err() 返回 nil;仅在流异常结束时返回错误。 207 func (w *WatchHandle) Err() error { 208 return w.err 209 } 210 211 // Stop 停止监听。 212 func (w *WatchHandle) Stop() { 213 w.cancel() 214 <-w.done 215 } 216 217 // Filesystem 提供沙箱文件系统操作。 218 type Filesystem struct { 219 sandbox *Sandbox 220 rpc filesystemconnect.FilesystemClient 221 } 222 223 // newFilesystem 创建 Filesystem 实例。 224 func newFilesystem(s *Sandbox) *Filesystem { 225 rpc := filesystemconnect.NewFilesystemClient( 226 s.client.config.HTTPClient, 227 s.envdURL(), 228 ) 229 return &Filesystem{sandbox: s, rpc: rpc} 230 } 231 232 // checkHTTPResponse 检查 HTTP 响应状态码,非 200 时读取 body 并返回 APIError。 233 func checkHTTPResponse(resp *http.Response) error { 234 if resp.StatusCode == http.StatusOK { 235 return nil 236 } 237 body, _ := io.ReadAll(resp.Body) 238 return newAPIError(resp, body) 239 } 240 241 // Read 读取指定路径的文件内容。 242 // 通过 envd HTTP API 下载文件。 243 func (fs *Filesystem) Read(ctx context.Context, path string, opts ...FilesystemOption) ([]byte, error) { 244 resp, err := fs.doRead(ctx, path, opts...) 245 if err != nil { 246 return nil, err 247 } 248 defer resp.Body.Close() 249 return io.ReadAll(resp.Body) 250 } 251 252 // ReadText 读取指定路径的文件内容,返回 string。 253 func (fs *Filesystem) ReadText(ctx context.Context, path string, opts ...FilesystemOption) (string, error) { 254 data, err := fs.Read(ctx, path, opts...) 255 if err != nil { 256 return "", err 257 } 258 return string(data), nil 259 } 260 261 // ReadStream 读取指定路径的文件内容,返回 io.ReadCloser 流。 262 // 调用方负责关闭返回的 ReadCloser。 263 func (fs *Filesystem) ReadStream(ctx context.Context, path string, opts ...FilesystemOption) (io.ReadCloser, error) { 264 resp, err := fs.doRead(ctx, path, opts...) 265 if err != nil { 266 return nil, err 267 } 268 return resp.Body, nil 269 } 270 271 // doRead 执行文件下载请求,返回原始 HTTP 响应。 272 // 调用方负责关闭 resp.Body。 273 func (fs *Filesystem) doRead(ctx context.Context, path string, opts ...FilesystemOption) (*http.Response, error) { 274 o := applyFilesystemOpts(opts) 275 downloadURL := fs.sandbox.DownloadURL(path, WithFileUser(o.user)) 276 277 req, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadURL, nil) 278 if err != nil { 279 return nil, fmt.Errorf("create request: %w", err) 280 } 281 setReqidHeader(ctx, req) 282 283 httpClient := fs.sandbox.client.config.HTTPClient 284 resp, err := httpClient.Do(req) 285 if err != nil { 286 return nil, fmt.Errorf("download file: %w", err) 287 } 288 289 if err := checkHTTPResponse(resp); err != nil { 290 resp.Body.Close() 291 return nil, err 292 } 293 294 return resp, nil 295 } 296 297 // Write 写入文件内容。如果文件已存在则覆盖,自动创建父目录。 298 // 通过 envd HTTP API 上传文件。 299 func (fs *Filesystem) Write(ctx context.Context, path string, data []byte, opts ...FilesystemOption) (*EntryInfo, error) { 300 o := applyFilesystemOpts(opts) 301 uploadURL := fs.sandbox.UploadURL(path, WithFileUser(o.user)) 302 303 pr, pw := io.Pipe() 304 writer := newMultipartWriter(pw) 305 306 go func() { 307 if err := writer.writeFile("file", path, data); err != nil { 308 pw.CloseWithError(err) 309 return 310 } 311 if err := writer.close(); err != nil { 312 pw.CloseWithError(err) 313 return 314 } 315 pw.Close() 316 }() 317 318 req, err := http.NewRequestWithContext(ctx, http.MethodPost, uploadURL, pr) 319 if err != nil { 320 return nil, fmt.Errorf("create request: %w", err) 321 } 322 req.Header.Set("Content-Type", writer.contentType()) 323 setReqidHeader(ctx, req) 324 325 httpClient := fs.sandbox.client.config.HTTPClient 326 resp, err := httpClient.Do(req) 327 if err != nil { 328 return nil, fmt.Errorf("upload file: %w", err) 329 } 330 defer resp.Body.Close() 331 332 if err := checkHTTPResponse(resp); err != nil { 333 return nil, err 334 } 335 336 // 上传成功后通过 Stat 获取文件信息 337 return fs.GetInfo(ctx, path, opts...) 338 } 339 340 // WriteEntry 批量写入的文件条目。 341 type WriteEntry struct { 342 Path string 343 Data []byte 344 } 345 346 // WriteFiles 批量写入多个文件。通过单次 multipart POST 请求上传。 347 // 单文件时 path 放在 query param(兼容现有 Write 行为); 348 // 多文件时每个 part 的 filename 为完整路径,服务端从 part filename 获取路径。 349 // 上传成功后逐个调用 GetInfo 获取文件元信息返回。 350 // 351 // 注意: 当前实现会对每个文件额外发起一次 GetInfo 请求(N+1), 352 // 大批量上传时需注意延迟,适合小批量场景。 353 func (fs *Filesystem) WriteFiles(ctx context.Context, files []WriteEntry, opts ...FilesystemOption) ([]*EntryInfo, error) { 354 if len(files) == 0 { 355 return nil, nil 356 } 357 358 // 单文件回退到 Write 359 if len(files) == 1 { 360 info, err := fs.Write(ctx, files[0].Path, files[0].Data, opts...) 361 if err != nil { 362 return nil, err 363 } 364 return []*EntryInfo{info}, nil 365 } 366 367 o := applyFilesystemOpts(opts) 368 uploadURL := fs.sandbox.batchUploadURL(o.user) 369 370 pr, pw := io.Pipe() 371 writer := newMultipartWriter(pw) 372 373 go func() { 374 for _, f := range files { 375 if err := writer.writeFileFullPath("file", f.Path, f.Data); err != nil { 376 pw.CloseWithError(err) 377 return 378 } 379 } 380 if err := writer.close(); err != nil { 381 pw.CloseWithError(err) 382 return 383 } 384 pw.Close() 385 }() 386 387 req, err := http.NewRequestWithContext(ctx, http.MethodPost, uploadURL, pr) 388 if err != nil { 389 return nil, fmt.Errorf("create request: %w", err) 390 } 391 req.Header.Set("Content-Type", writer.contentType()) 392 setReqidHeader(ctx, req) 393 394 httpClient := fs.sandbox.client.config.HTTPClient 395 resp, err := httpClient.Do(req) 396 if err != nil { 397 return nil, fmt.Errorf("upload files: %w", err) 398 } 399 defer resp.Body.Close() 400 401 if err := checkHTTPResponse(resp); err != nil { 402 return nil, err 403 } 404 405 // 逐个获取文件元信息 406 infos := make([]*EntryInfo, 0, len(files)) 407 for _, f := range files { 408 info, err := fs.GetInfo(ctx, f.Path, opts...) 409 if err != nil { 410 return nil, fmt.Errorf("get info for %s: %w", f.Path, err) 411 } 412 infos = append(infos, info) 413 } 414 return infos, nil 415 } 416 417 // List 列出目录内容。 418 func (fs *Filesystem) List(ctx context.Context, path string, opts ...ListOption) ([]EntryInfo, error) { 419 o := applyListOpts(opts) 420 req := connect.NewRequest(&filesystem.ListDirRequest{ 421 Path: path, 422 Depth: o.depth, 423 }) 424 fs.sandbox.setEnvdAuth(req, o.user) 425 426 resp, err := fs.rpc.ListDir(ctx, req) 427 if err != nil { 428 return nil, fmt.Errorf("list dir: %w", err) 429 } 430 431 entries := make([]EntryInfo, 0, len(resp.Msg.Entries)) 432 for _, e := range resp.Msg.Entries { 433 info := entryInfoFromProto(e) 434 if info == nil { 435 continue 436 } 437 entries = append(entries, *info) 438 } 439 return entries, nil 440 } 441 442 // Exists 检查文件或目录是否存在。 443 func (fs *Filesystem) Exists(ctx context.Context, path string, opts ...FilesystemOption) (bool, error) { 444 _, err := fs.GetInfo(ctx, path, opts...) 445 if err != nil { 446 if isNotFoundError(err) { 447 return false, nil 448 } 449 return false, err 450 } 451 return true, nil 452 } 453 454 // GetInfo 返回文件或目录的元信息。 455 func (fs *Filesystem) GetInfo(ctx context.Context, path string, opts ...FilesystemOption) (*EntryInfo, error) { 456 o := applyFilesystemOpts(opts) 457 req := connect.NewRequest(&filesystem.StatRequest{Path: path}) 458 fs.sandbox.setEnvdAuth(req, o.user) 459 460 resp, err := fs.rpc.Stat(ctx, req) 461 if err != nil { 462 return nil, fmt.Errorf("stat: %w", err) 463 } 464 return entryInfoFromProto(resp.Msg.Entry), nil 465 } 466 467 // MakeDir 创建目录(包含父目录)。 468 func (fs *Filesystem) MakeDir(ctx context.Context, path string, opts ...FilesystemOption) (*EntryInfo, error) { 469 o := applyFilesystemOpts(opts) 470 req := connect.NewRequest(&filesystem.MakeDirRequest{Path: path}) 471 fs.sandbox.setEnvdAuth(req, o.user) 472 473 resp, err := fs.rpc.MakeDir(ctx, req) 474 if err != nil { 475 return nil, fmt.Errorf("mkdir: %w", err) 476 } 477 return entryInfoFromProto(resp.Msg.Entry), nil 478 } 479 480 // Remove 删除文件或目录。 481 func (fs *Filesystem) Remove(ctx context.Context, path string, opts ...FilesystemOption) error { 482 o := applyFilesystemOpts(opts) 483 req := connect.NewRequest(&filesystem.RemoveRequest{Path: path}) 484 fs.sandbox.setEnvdAuth(req, o.user) 485 486 _, err := fs.rpc.Remove(ctx, req) 487 if err != nil { 488 return fmt.Errorf("remove: %w", err) 489 } 490 return nil 491 } 492 493 // Rename 重命名或移动文件/目录。 494 func (fs *Filesystem) Rename(ctx context.Context, oldPath, newPath string, opts ...FilesystemOption) (*EntryInfo, error) { 495 o := applyFilesystemOpts(opts) 496 req := connect.NewRequest(&filesystem.MoveRequest{ 497 Source: oldPath, 498 Destination: newPath, 499 }) 500 fs.sandbox.setEnvdAuth(req, o.user) 501 502 resp, err := fs.rpc.Move(ctx, req) 503 if err != nil { 504 return nil, fmt.Errorf("move: %w", err) 505 } 506 return entryInfoFromProto(resp.Msg.Entry), nil 507 } 508 509 // WatchDir 监听目录变更。返回 WatchHandle 用于接收事件和停止监听。 510 func (fs *Filesystem) WatchDir(ctx context.Context, path string, opts ...WatchOption) (*WatchHandle, error) { 511 o := applyWatchOpts(opts) 512 513 watchCtx, cancel := context.WithCancel(ctx) 514 req := connect.NewRequest(&filesystem.WatchDirRequest{ 515 Path: path, 516 Recursive: o.recursive, 517 }) 518 fs.sandbox.setEnvdAuth(req, o.user) 519 520 stream, err := fs.rpc.WatchDir(watchCtx, req) 521 if err != nil { 522 cancel() 523 return nil, fmt.Errorf("watch dir: %w", err) 524 } 525 526 w := &WatchHandle{ 527 events: make(chan FilesystemEvent, 64), 528 cancel: cancel, 529 done: make(chan struct{}), 530 } 531 532 go func() { 533 defer close(w.done) 534 defer close(w.events) 535 for stream.Receive() { 536 msg := stream.Msg() 537 if fsEvent := msg.GetFilesystem(); fsEvent != nil { 538 ev := filesystemEventFromProto(fsEvent) 539 select { 540 case w.events <- ev: 541 case <-watchCtx.Done(): 542 return 543 } 544 } 545 } 546 if err := stream.Err(); err != nil && watchCtx.Err() == nil { 547 w.err = err 548 } 549 }() 550 551 return w, nil 552 }