github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/fs/metadata.go (about) 1 package fs 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "fmt" 8 "os/exec" 9 "strings" 10 "time" 11 ) 12 13 // Metadata represents Object metadata in a standardised form 14 // 15 // See docs/content/metadata.md for the interpretation of the keys 16 type Metadata map[string]string 17 18 // MetadataHelp represents help for a bit of system metadata 19 type MetadataHelp struct { 20 Help string 21 Type string 22 Example string 23 ReadOnly bool 24 } 25 26 // MetadataInfo is help for the whole metadata for this backend. 27 type MetadataInfo struct { 28 System map[string]MetadataHelp 29 Help string 30 } 31 32 // Set k to v on m 33 // 34 // If m is nil, then it will get made 35 func (m *Metadata) Set(k, v string) { 36 if *m == nil { 37 *m = make(Metadata, 1) 38 } 39 (*m)[k] = v 40 } 41 42 // Merge other into m 43 // 44 // If m is nil, then it will get made 45 func (m *Metadata) Merge(other Metadata) { 46 for k, v := range other { 47 if *m == nil { 48 *m = make(Metadata, len(other)) 49 } 50 (*m)[k] = v 51 } 52 } 53 54 // MergeOptions gets any Metadata from the options passed in and 55 // stores it in m (which may be nil). 56 // 57 // If there is no m then metadata will be nil 58 func (m *Metadata) MergeOptions(options []OpenOption) { 59 for _, opt := range options { 60 if metadataOption, ok := opt.(MetadataOption); ok { 61 m.Merge(Metadata(metadataOption)) 62 } 63 } 64 } 65 66 // GetMetadata from an DirEntry 67 // 68 // If the object has no metadata then metadata will be nil 69 func GetMetadata(ctx context.Context, o DirEntry) (metadata Metadata, err error) { 70 do, ok := o.(Metadataer) 71 if !ok { 72 return nil, nil 73 } 74 return do.Metadata(ctx) 75 } 76 77 // mapItem descripts the item to be mapped 78 type mapItem struct { 79 SrcFs string 80 SrcFsType string 81 DstFs string 82 DstFsType string 83 Remote string 84 Size int64 85 MimeType string `json:",omitempty"` 86 ModTime time.Time 87 IsDir bool 88 ID string `json:",omitempty"` 89 Metadata Metadata `json:",omitempty"` 90 } 91 92 // This runs an external program on the metadata which can be used to 93 // map it from one form to another. 94 func metadataMapper(ctx context.Context, cmdLine SpaceSepList, dstFs Fs, o DirEntry, metadata Metadata) (newMetadata Metadata, err error) { 95 ci := GetConfig(ctx) 96 cmd := exec.Command(cmdLine[0], cmdLine[1:]...) 97 in := mapItem{ 98 DstFs: ConfigString(dstFs), 99 DstFsType: Type(dstFs), 100 Remote: o.Remote(), 101 Size: o.Size(), 102 MimeType: MimeType(ctx, o), 103 ModTime: o.ModTime(ctx), 104 IsDir: false, 105 Metadata: metadata, 106 } 107 fInfo := o.Fs() 108 if f, ok := fInfo.(Fs); ok { 109 in.SrcFs = ConfigString(f) 110 in.SrcFsType = Type(f) 111 } else { 112 in.SrcFs = fInfo.Name() + ":" + fInfo.Root() 113 in.SrcFsType = "unknown" 114 } 115 if do, ok := o.(IDer); ok { 116 in.ID = do.ID() 117 } 118 inBytes, err := json.MarshalIndent(in, "", "\t") 119 if err != nil { 120 return nil, fmt.Errorf("metadata mapper: failed to marshal input: %w", err) 121 } 122 if ci.Dump.IsSet(DumpMapper) { 123 Debugf(nil, "Metadata mapper sent: \n%s\n", string(inBytes)) 124 } 125 var stdout, stderr bytes.Buffer 126 cmd.Stdin = bytes.NewBuffer(inBytes) 127 cmd.Stdout = &stdout 128 cmd.Stderr = &stderr 129 start := time.Now() 130 err = cmd.Run() 131 Debugf(o, "Calling metadata mapper %v", cmdLine) 132 duration := time.Since(start) 133 if err != nil { 134 return nil, fmt.Errorf("metadata mapper: failed on %v: %q: %w", cmdLine, strings.TrimSpace(stderr.String()), err) 135 } 136 if ci.Dump.IsSet(DumpMapper) { 137 Debugf(nil, "Metadata mapper received: \n%s\n", stdout.String()) 138 } 139 var out mapItem 140 err = json.Unmarshal(stdout.Bytes(), &out) 141 if err != nil { 142 return nil, fmt.Errorf("metadata mapper: failed to read output: %q: %w", stdout.String(), err) 143 } 144 Debugf(o, "Metadata mapper returned in %v", duration) 145 return out.Metadata, nil 146 } 147 148 // GetMetadataOptions from an DirEntry and merge it with any in options 149 // 150 // If --metadata isn't in use it will return nil. 151 // 152 // If the object has no metadata then metadata will be nil. 153 // 154 // This should be passed the destination Fs for the metadata mapper 155 func GetMetadataOptions(ctx context.Context, dstFs Fs, o DirEntry, options []OpenOption) (metadata Metadata, err error) { 156 ci := GetConfig(ctx) 157 if !ci.Metadata { 158 return nil, nil 159 } 160 metadata, err = GetMetadata(ctx, o) 161 if err != nil { 162 return nil, err 163 } 164 metadata.MergeOptions(options) 165 if len(ci.MetadataMapper) != 0 { 166 metadata, err = metadataMapper(ctx, ci.MetadataMapper, dstFs, o, metadata) 167 if err != nil { 168 return nil, err 169 } 170 } 171 return metadata, nil 172 }