github.com/databricks/cli@v0.203.0/libs/filer/files_client.go (about) 1 package filer 2 3 import ( 4 "bytes" 5 "context" 6 "errors" 7 "fmt" 8 "io" 9 "io/fs" 10 "net/http" 11 "net/url" 12 "path" 13 "strings" 14 "time" 15 16 "github.com/databricks/databricks-sdk-go" 17 "github.com/databricks/databricks-sdk-go/apierr" 18 "github.com/databricks/databricks-sdk-go/client" 19 "golang.org/x/exp/slices" 20 ) 21 22 // Type that implements fs.FileInfo for the Files API. 23 type filesApiFileInfo struct { 24 absPath string 25 isDir bool 26 } 27 28 func (info filesApiFileInfo) Name() string { 29 return path.Base(info.absPath) 30 } 31 32 func (info filesApiFileInfo) Size() int64 { 33 // No way to get the file size in the Files API. 34 return 0 35 } 36 37 func (info filesApiFileInfo) Mode() fs.FileMode { 38 mode := fs.ModePerm 39 if info.isDir { 40 mode |= fs.ModeDir 41 } 42 return mode 43 } 44 45 func (info filesApiFileInfo) ModTime() time.Time { 46 return time.Time{} 47 } 48 49 func (info filesApiFileInfo) IsDir() bool { 50 return info.isDir 51 } 52 53 func (info filesApiFileInfo) Sys() any { 54 return nil 55 } 56 57 // FilesClient implements the [Filer] interface for the Files API backend. 58 type FilesClient struct { 59 workspaceClient *databricks.WorkspaceClient 60 apiClient *client.DatabricksClient 61 62 // File operations will be relative to this path. 63 root WorkspaceRootPath 64 } 65 66 func filesNotImplementedError(fn string) error { 67 return fmt.Errorf("filer.%s is not implemented for the Files API", fn) 68 } 69 70 func NewFilesClient(w *databricks.WorkspaceClient, root string) (Filer, error) { 71 apiClient, err := client.New(w.Config) 72 if err != nil { 73 return nil, err 74 } 75 76 return &FilesClient{ 77 workspaceClient: w, 78 apiClient: apiClient, 79 80 root: NewWorkspaceRootPath(root), 81 }, nil 82 } 83 84 func (w *FilesClient) urlPath(name string) (string, string, error) { 85 absPath, err := w.root.Join(name) 86 if err != nil { 87 return "", "", err 88 } 89 90 // The user specified part of the path must be escaped. 91 urlPath := fmt.Sprintf( 92 "/api/2.0/fs/files/%s", 93 url.PathEscape(strings.TrimLeft(absPath, "/")), 94 ) 95 96 return absPath, urlPath, nil 97 } 98 99 func (w *FilesClient) Write(ctx context.Context, name string, reader io.Reader, mode ...WriteMode) error { 100 absPath, urlPath, err := w.urlPath(name) 101 if err != nil { 102 return err 103 } 104 105 overwrite := slices.Contains(mode, OverwriteIfExists) 106 urlPath = fmt.Sprintf("%s?overwrite=%t", urlPath, overwrite) 107 err = w.apiClient.Do(ctx, http.MethodPut, urlPath, reader, nil, 108 func(r *http.Request) error { 109 r.Header.Set("Content-Type", "application/octet-stream") 110 return nil 111 }) 112 113 // Return early on success. 114 if err == nil { 115 return nil 116 } 117 118 // Special handling of this error only if it is an API error. 119 var aerr *apierr.APIError 120 if !errors.As(err, &aerr) { 121 return err 122 } 123 124 // This API returns 409 if the file already exists, when the object type is file 125 if aerr.StatusCode == http.StatusConflict { 126 return FileAlreadyExistsError{absPath} 127 } 128 129 return err 130 } 131 132 func (w *FilesClient) Read(ctx context.Context, name string) (io.ReadCloser, error) { 133 absPath, urlPath, err := w.urlPath(name) 134 if err != nil { 135 return nil, err 136 } 137 138 var buf bytes.Buffer 139 err = w.apiClient.Do(ctx, http.MethodGet, urlPath, nil, &buf) 140 141 // Return early on success. 142 if err == nil { 143 return io.NopCloser(&buf), nil 144 } 145 146 // Special handling of this error only if it is an API error. 147 var aerr *apierr.APIError 148 if !errors.As(err, &aerr) { 149 return nil, err 150 } 151 152 // This API returns a 404 if the specified path does not exist. 153 if aerr.StatusCode == http.StatusNotFound { 154 return nil, FileDoesNotExistError{absPath} 155 } 156 157 return nil, err 158 } 159 160 func (w *FilesClient) Delete(ctx context.Context, name string, mode ...DeleteMode) error { 161 absPath, urlPath, err := w.urlPath(name) 162 if err != nil { 163 return err 164 } 165 166 // Illegal to delete the root path. 167 if absPath == w.root.rootPath { 168 return CannotDeleteRootError{} 169 } 170 171 err = w.apiClient.Do(ctx, http.MethodDelete, urlPath, nil, nil) 172 173 // Return early on success. 174 if err == nil { 175 return nil 176 } 177 178 // Special handling of this error only if it is an API error. 179 var aerr *apierr.APIError 180 if !errors.As(err, &aerr) { 181 return err 182 } 183 184 // This API returns a 404 if the specified path does not exist. 185 if aerr.StatusCode == http.StatusNotFound { 186 return FileDoesNotExistError{absPath} 187 } 188 189 // This API returns 409 if the underlying path is a directory. 190 if aerr.StatusCode == http.StatusConflict { 191 return DirectoryNotEmptyError{absPath} 192 } 193 194 return err 195 } 196 197 func (w *FilesClient) ReadDir(ctx context.Context, name string) ([]fs.DirEntry, error) { 198 return nil, filesNotImplementedError("ReadDir") 199 } 200 201 func (w *FilesClient) Mkdir(ctx context.Context, name string) error { 202 // Directories are created implicitly. 203 // No need to do anything. 204 return nil 205 } 206 207 func (w *FilesClient) Stat(ctx context.Context, name string) (fs.FileInfo, error) { 208 absPath, urlPath, err := w.urlPath(name) 209 if err != nil { 210 return nil, err 211 } 212 213 err = w.apiClient.Do(ctx, http.MethodHead, urlPath, nil, nil, 214 func(r *http.Request) error { 215 r.Header.Del("Content-Type") 216 return nil 217 }) 218 219 // If the HEAD requests succeeds, the file exists. 220 if err == nil { 221 return filesApiFileInfo{absPath: absPath, isDir: false}, nil 222 } 223 224 // Special handling of this error only if it is an API error. 225 var aerr *apierr.APIError 226 if !errors.As(err, &aerr) { 227 return nil, err 228 } 229 230 // This API returns a 404 if the specified path does not exist. 231 if aerr.StatusCode == http.StatusNotFound { 232 return nil, FileDoesNotExistError{absPath} 233 } 234 235 // This API returns 409 if the underlying path is a directory. 236 if aerr.StatusCode == http.StatusConflict { 237 return filesApiFileInfo{absPath: absPath, isDir: true}, nil 238 } 239 240 return nil, err 241 }