github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/fs/operations/lsjson.go (about) 1 package operations 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "path" 8 "strings" 9 "time" 10 11 "github.com/rclone/rclone/backend/crypt" 12 "github.com/rclone/rclone/fs" 13 "github.com/rclone/rclone/fs/hash" 14 "github.com/rclone/rclone/fs/walk" 15 ) 16 17 // ListJSONItem in the struct which gets marshalled for each line 18 type ListJSONItem struct { 19 Path string 20 Name string 21 EncryptedPath string `json:",omitempty"` 22 Encrypted string `json:",omitempty"` 23 Size int64 24 MimeType string `json:",omitempty"` 25 ModTime Timestamp //`json:",omitempty"` 26 IsDir bool 27 Hashes map[string]string `json:",omitempty"` 28 ID string `json:",omitempty"` 29 OrigID string `json:",omitempty"` 30 Tier string `json:",omitempty"` 31 IsBucket bool `json:",omitempty"` 32 Metadata fs.Metadata `json:",omitempty"` 33 } 34 35 // Timestamp a time in the provided format 36 type Timestamp struct { 37 When time.Time 38 Format string 39 } 40 41 // MarshalJSON turns a Timestamp into JSON 42 func (t Timestamp) MarshalJSON() (out []byte, err error) { 43 if t.When.IsZero() { 44 return []byte(`""`), nil 45 } 46 return []byte(`"` + t.When.Format(t.Format) + `"`), nil 47 } 48 49 // Returns a time format for the given precision 50 func formatForPrecision(precision time.Duration) string { 51 switch { 52 case precision <= time.Nanosecond: 53 return "2006-01-02T15:04:05.000000000Z07:00" 54 case precision <= 10*time.Nanosecond: 55 return "2006-01-02T15:04:05.00000000Z07:00" 56 case precision <= 100*time.Nanosecond: 57 return "2006-01-02T15:04:05.0000000Z07:00" 58 case precision <= time.Microsecond: 59 return "2006-01-02T15:04:05.000000Z07:00" 60 case precision <= 10*time.Microsecond: 61 return "2006-01-02T15:04:05.00000Z07:00" 62 case precision <= 100*time.Microsecond: 63 return "2006-01-02T15:04:05.0000Z07:00" 64 case precision <= time.Millisecond: 65 return "2006-01-02T15:04:05.000Z07:00" 66 case precision <= 10*time.Millisecond: 67 return "2006-01-02T15:04:05.00Z07:00" 68 case precision <= 100*time.Millisecond: 69 return "2006-01-02T15:04:05.0Z07:00" 70 } 71 return time.RFC3339 72 } 73 74 // ListJSONOpt describes the options for ListJSON 75 type ListJSONOpt struct { 76 Recurse bool `json:"recurse"` 77 NoModTime bool `json:"noModTime"` 78 NoMimeType bool `json:"noMimeType"` 79 ShowEncrypted bool `json:"showEncrypted"` 80 ShowOrigIDs bool `json:"showOrigIDs"` 81 ShowHash bool `json:"showHash"` 82 DirsOnly bool `json:"dirsOnly"` 83 FilesOnly bool `json:"filesOnly"` 84 Metadata bool `json:"metadata"` 85 HashTypes []string `json:"hashTypes"` // hash types to show if ShowHash is set, e.g. "MD5", "SHA-1" 86 } 87 88 // state for ListJson 89 type listJSON struct { 90 fsrc fs.Fs 91 remote string 92 format string 93 opt *ListJSONOpt 94 cipher *crypt.Cipher 95 hashTypes []hash.Type 96 dirs bool 97 files bool 98 canGetTier bool 99 isBucket bool 100 showHash bool 101 } 102 103 func newListJSON(ctx context.Context, fsrc fs.Fs, remote string, opt *ListJSONOpt) (*listJSON, error) { 104 lj := &listJSON{ 105 fsrc: fsrc, 106 remote: remote, 107 opt: opt, 108 dirs: true, 109 files: true, 110 } 111 // Dirs Files 112 // !FilesOnly,!DirsOnly true true 113 // !FilesOnly,DirsOnly true false 114 // FilesOnly,!DirsOnly false true 115 // FilesOnly,DirsOnly true true 116 if !opt.FilesOnly && opt.DirsOnly { 117 lj.files = false 118 } else if opt.FilesOnly && !opt.DirsOnly { 119 lj.dirs = false 120 } 121 if opt.ShowEncrypted { 122 fsInfo, _, _, config, err := fs.ConfigFs(fs.ConfigStringFull(fsrc)) 123 if err != nil { 124 return nil, fmt.Errorf("ListJSON failed to load config for crypt remote: %w", err) 125 } 126 if fsInfo.Name != "crypt" { 127 return nil, errors.New("the remote needs to be of type \"crypt\"") 128 } 129 lj.cipher, err = crypt.NewCipher(config) 130 if err != nil { 131 return nil, fmt.Errorf("ListJSON failed to make new crypt remote: %w", err) 132 } 133 } 134 features := fsrc.Features() 135 lj.canGetTier = features.GetTier 136 lj.format = formatForPrecision(fsrc.Precision()) 137 lj.isBucket = features.BucketBased && remote == "" && fsrc.Root() == "" // if bucket-based remote listing the root mark directories as buckets 138 lj.showHash = opt.ShowHash 139 lj.hashTypes = fsrc.Hashes().Array() 140 if len(opt.HashTypes) != 0 { 141 lj.showHash = true 142 lj.hashTypes = []hash.Type{} 143 for _, hashType := range opt.HashTypes { 144 var ht hash.Type 145 err := ht.Set(hashType) 146 if err != nil { 147 return nil, err 148 } 149 lj.hashTypes = append(lj.hashTypes, ht) 150 } 151 } 152 return lj, nil 153 } 154 155 // Convert a single entry to JSON 156 // 157 // It may return nil if there is no entry to return 158 func (lj *listJSON) entry(ctx context.Context, entry fs.DirEntry) (*ListJSONItem, error) { 159 switch entry.(type) { 160 case fs.Directory: 161 if lj.opt.FilesOnly { 162 return nil, nil 163 } 164 case fs.Object: 165 if lj.opt.DirsOnly { 166 return nil, nil 167 } 168 default: 169 fs.Errorf(nil, "Unknown type %T in listing", entry) 170 } 171 172 item := &ListJSONItem{ 173 Path: entry.Remote(), 174 Name: path.Base(entry.Remote()), 175 Size: entry.Size(), 176 } 177 if entry.Remote() == "" { 178 item.Name = "" 179 } 180 if !lj.opt.NoModTime { 181 item.ModTime = Timestamp{When: entry.ModTime(ctx), Format: lj.format} 182 } 183 if !lj.opt.NoMimeType { 184 item.MimeType = fs.MimeTypeDirEntry(ctx, entry) 185 } 186 if lj.cipher != nil { 187 switch entry.(type) { 188 case fs.Directory: 189 item.EncryptedPath = lj.cipher.EncryptDirName(entry.Remote()) 190 case fs.Object: 191 item.EncryptedPath = lj.cipher.EncryptFileName(entry.Remote()) 192 default: 193 fs.Errorf(nil, "Unknown type %T in listing", entry) 194 } 195 item.Encrypted = path.Base(item.EncryptedPath) 196 } 197 if lj.opt.Metadata { 198 metadata, err := fs.GetMetadata(ctx, entry) 199 if err != nil { 200 fs.Errorf(entry, "Failed to read metadata: %v", err) 201 } else if metadata != nil { 202 item.Metadata = metadata 203 } 204 } 205 if do, ok := entry.(fs.IDer); ok { 206 item.ID = do.ID() 207 } 208 if o, ok := entry.(fs.Object); lj.opt.ShowOrigIDs && ok { 209 if do, ok := fs.UnWrapObject(o).(fs.IDer); ok { 210 item.OrigID = do.ID() 211 } 212 } 213 switch x := entry.(type) { 214 case fs.Directory: 215 item.IsDir = true 216 item.IsBucket = lj.isBucket 217 case fs.Object: 218 item.IsDir = false 219 if lj.showHash { 220 item.Hashes = make(map[string]string) 221 for _, hashType := range lj.hashTypes { 222 hash, err := x.Hash(ctx, hashType) 223 if err != nil { 224 fs.Errorf(x, "Failed to read hash: %v", err) 225 } else if hash != "" { 226 item.Hashes[hashType.String()] = hash 227 } 228 } 229 } 230 if lj.canGetTier { 231 if do, ok := x.(fs.GetTierer); ok { 232 item.Tier = do.GetTier() 233 } 234 } 235 default: 236 fs.Errorf(nil, "Unknown type %T in listing in ListJSON", entry) 237 } 238 return item, nil 239 } 240 241 // ListJSON lists fsrc using the options in opt calling callback for each item 242 func ListJSON(ctx context.Context, fsrc fs.Fs, remote string, opt *ListJSONOpt, callback func(*ListJSONItem) error) error { 243 lj, err := newListJSON(ctx, fsrc, remote, opt) 244 if err != nil { 245 return err 246 } 247 err = walk.ListR(ctx, fsrc, remote, false, ConfigMaxDepth(ctx, lj.opt.Recurse), walk.ListAll, func(entries fs.DirEntries) (err error) { 248 for _, entry := range entries { 249 item, err := lj.entry(ctx, entry) 250 if err != nil { 251 return fmt.Errorf("creating entry failed in ListJSON: %w", err) 252 } 253 if item != nil { 254 err = callback(item) 255 if err != nil { 256 return fmt.Errorf("callback failed in ListJSON: %w", err) 257 } 258 } 259 } 260 return nil 261 }) 262 if err != nil { 263 return fmt.Errorf("error in ListJSON: %w", err) 264 } 265 return nil 266 } 267 268 // StatJSON returns a single JSON stat entry for the fsrc, remote path 269 // 270 // The item returned may be nil if it is not found or excluded with DirsOnly/FilesOnly 271 func StatJSON(ctx context.Context, fsrc fs.Fs, remote string, opt *ListJSONOpt) (item *ListJSONItem, err error) { 272 // FIXME this could me more efficient we had a new primitive 273 // NewDirEntry() which returned an Object or a Directory 274 lj, err := newListJSON(ctx, fsrc, remote, opt) 275 if err != nil { 276 return nil, err 277 } 278 279 // Root is always a directory. When we have a NewDirEntry 280 // primitive we need to call it, but for now this will do. 281 if remote == "" { 282 if !lj.dirs { 283 return nil, nil 284 } 285 // Check the root directory exists 286 _, err := fsrc.List(ctx, "") 287 if err != nil { 288 return nil, err 289 } 290 return lj.entry(ctx, fs.NewDir("", time.Now())) 291 } 292 293 // Could be a file or a directory here 294 if lj.files && !strings.HasSuffix(remote, "/") { 295 // NewObject can return the sentinel errors ErrorObjectNotFound or ErrorIsDir 296 // ErrorObjectNotFound can mean the source is a directory or not found 297 obj, err := fsrc.NewObject(ctx, remote) 298 if err == fs.ErrorObjectNotFound { 299 if !lj.dirs { 300 return nil, nil 301 } 302 } else if err == fs.ErrorIsDir { 303 if !lj.dirs { 304 return nil, nil 305 } 306 // This could return a made up ListJSONItem here 307 // but that wouldn't have the IDs etc in 308 } else if err != nil { 309 if !lj.dirs { 310 return nil, err 311 } 312 } else { 313 return lj.entry(ctx, obj) 314 } 315 } 316 // Must be a directory here 317 // 318 // Remove trailing / as rclone listings won't have them 319 remote = strings.TrimRight(remote, "/") 320 parent := path.Dir(remote) 321 if parent == "." || parent == "/" { 322 parent = "" 323 } 324 entries, err := fsrc.List(ctx, parent) 325 if err == fs.ErrorDirNotFound { 326 return nil, nil 327 } else if err != nil { 328 return nil, err 329 } 330 equal := func(a, b string) bool { return a == b } 331 if fsrc.Features().CaseInsensitive { 332 equal = strings.EqualFold 333 } 334 var foundEntry fs.DirEntry 335 for _, entry := range entries { 336 if equal(entry.Remote(), remote) { 337 foundEntry = entry 338 break 339 } 340 } 341 if foundEntry == nil { 342 return nil, nil 343 } 344 return lj.entry(ctx, foundEntry) 345 }