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  }