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  }