github.com/viant/toolbox@v0.34.5/storage/copy.go (about)

     1  package storage
     2  
     3  import (
     4  	"archive/tar"
     5  	"archive/zip"
     6  	"bytes"
     7  	"fmt"
     8  	"github.com/viant/toolbox"
     9  	"io"
    10  	"io/ioutil"
    11  	"path"
    12  	"strings"
    13  )
    14  
    15  type CopyHandler func(sourceObject Object, source io.Reader, destinationService Service, destinationURL string) error
    16  type ModificationHandler func(reader io.ReadCloser) (io.ReadCloser, error)
    17  
    18  func urlPath(URL string) string {
    19  	var result = URL
    20  	schemaPosition := strings.Index(URL, "://")
    21  	if schemaPosition != -1 {
    22  		result = string(URL[schemaPosition+3:])
    23  	}
    24  	pathRoot := strings.Index(result, "/")
    25  	if pathRoot > 0 {
    26  		result = string(result[pathRoot:])
    27  	}
    28  	if strings.HasSuffix(result, "/") {
    29  		result = string(result[:len(result)-1])
    30  	}
    31  
    32  	return result
    33  }
    34  
    35  func truncatePath(path string) string {
    36  	if len(path) <= 1 {
    37  		return path
    38  	}
    39  	if strings.HasSuffix(path, "/") {
    40  		return string(path[:len(path)-1])
    41  	}
    42  	return path
    43  }
    44  
    45  func copyStorageContent(sourceService Service, sourceURL string, destinationService Service, destinationURL string, modifyContentHandler ModificationHandler, subPath string, copyHandler CopyHandler) error {
    46  	sourceListURL := sourceURL
    47  	if subPath != "" {
    48  		sourceListURL = toolbox.URLPathJoin(sourceURL, subPath)
    49  	}
    50  	objects, err := sourceService.List(sourceListURL)
    51  	if err != nil {
    52  		return err
    53  	}
    54  
    55  	for _, object := range objects {
    56  		if err = copyObject(object, sourceService, sourceURL, destinationService, destinationURL, modifyContentHandler, subPath, copyHandler); err != nil {
    57  			return err
    58  		}
    59  	}
    60  	return nil
    61  }
    62  
    63  func copyObject(object Object, sourceService Service, sourceURL string, destinationService Service, destinationURL string, modifyContentHandler ModificationHandler, subPath string, copyHandler CopyHandler) error {
    64  	var objectRelativePath string
    65  	sourceURLPath := urlPath(sourceURL)
    66  
    67  	var objectURLPath = urlPath(object.URL())
    68  	if object.IsFolder() {
    69  		if truncatePath(sourceURLPath) == truncatePath(objectURLPath) {
    70  			return nil
    71  		}
    72  		if subPath != "" && objectURLPath == toolbox.URLPathJoin(sourceURLPath, subPath) {
    73  			return nil
    74  		}
    75  	}
    76  	if len(objectURLPath) > len(sourceURLPath) {
    77  		objectRelativePath = objectURLPath[len(sourceURLPath):]
    78  		if strings.HasPrefix(objectRelativePath, "/") {
    79  			objectRelativePath = string(objectRelativePath[1:])
    80  		}
    81  	}
    82  	var destinationObjectURL = destinationURL
    83  	if objectRelativePath != "" {
    84  		destinationObjectURL = toolbox.URLPathJoin(destinationURL, objectRelativePath)
    85  	}
    86  
    87  	if object.IsContent() {
    88  		reader, err := sourceService.Download(object)
    89  		if err != nil {
    90  			err = fmt.Errorf("unable download, %v -> %v, %v", object.URL(), destinationObjectURL, err)
    91  			return err
    92  		}
    93  		defer reader.Close()
    94  
    95  		if modifyContentHandler != nil {
    96  			content, err := ioutil.ReadAll(reader)
    97  			if err != nil {
    98  				return err
    99  			}
   100  			reader = ioutil.NopCloser(bytes.NewReader(content))
   101  			reader, err = modifyContentHandler(reader)
   102  			if err != nil {
   103  				err = fmt.Errorf("unable modify content, %v %v %v", object.URL(), destinationObjectURL, err)
   104  				return err
   105  			}
   106  		}
   107  
   108  		if subPath == "" {
   109  			_, sourceName := path.Split(object.URL())
   110  			_, destinationName := path.Split(destinationURL)
   111  			if strings.HasSuffix(destinationObjectURL, "/") {
   112  				destinationObjectURL = toolbox.URLPathJoin(destinationObjectURL, sourceName)
   113  			} else {
   114  				destinationObject, _ := destinationService.StorageObject(destinationObjectURL)
   115  				if destinationObject != nil && destinationObject.IsFolder() {
   116  					destinationObjectURL = toolbox.URLPathJoin(destinationObjectURL, sourceName)
   117  				} else if destinationName != sourceName {
   118  					if !strings.Contains(destinationName, ".") {
   119  						destinationObjectURL = toolbox.URLPathJoin(destinationURL, sourceName)
   120  					}
   121  
   122  				}
   123  			}
   124  		}
   125  		err = copyHandler(object, reader, destinationService, destinationObjectURL)
   126  		if err != nil {
   127  			return err
   128  		}
   129  
   130  	} else {
   131  		if err := copyStorageContent(sourceService, sourceURL, destinationService, destinationURL, modifyContentHandler, objectRelativePath, copyHandler); err != nil {
   132  			return err
   133  		}
   134  	}
   135  	return nil
   136  }
   137  
   138  func copySourceToDestination(sourceObject Object, reader io.Reader, destinationService Service, destinationURL string) error {
   139  	mode := DefaultFileMode
   140  	if fileInfo := sourceObject.FileInfo(); fileInfo != nil {
   141  		mode = fileInfo.Mode()
   142  	}
   143  	err := destinationService.UploadWithMode(destinationURL, mode, reader)
   144  	if err != nil {
   145  		err = fmt.Errorf("unable upload, %v %v %v", sourceObject.URL(), destinationURL, err)
   146  	}
   147  	return err
   148  }
   149  
   150  func getArchiveCopyHandler(archive *zip.Writer, parentURL string) CopyHandler {
   151  
   152  	return func(sourceObject Object, reader io.Reader, destinationService Service, destinationURL string) error {
   153  		var _, relativePath = toolbox.URLSplit(destinationURL)
   154  		if destinationURL != parentURL {
   155  			relativePath = strings.Replace(destinationURL, parentURL, "", 1)
   156  		}
   157  
   158  		header, err := zip.FileInfoHeader(sourceObject.FileInfo())
   159  		if err != nil {
   160  			return err
   161  		}
   162  		header.Method = zip.Deflate
   163  		header.Name = relativePath
   164  		writer, err := archive.CreateHeader(header)
   165  		if err != nil {
   166  			return err
   167  		}
   168  		_, err = io.Copy(writer, reader)
   169  		return err
   170  	}
   171  }
   172  
   173  //Copy downloads objects from source URL to upload them to destination URL.
   174  func Copy(sourceService Service, sourceURL string, destinationService Service, destinationURL string, modifyContentHandler ModificationHandler, copyHandler CopyHandler) (err error) {
   175  	if copyHandler == nil {
   176  		copyHandler = copySourceToDestination
   177  	}
   178  	if strings.HasSuffix(sourceURL, "//") {
   179  		sourceURL = string(sourceURL[:len(sourceURL)-1])
   180  	}
   181  	err = copyStorageContent(sourceService, sourceURL, destinationService, destinationURL, modifyContentHandler, "", copyHandler)
   182  	if err != nil {
   183  		err = fmt.Errorf("failed to copy %v -> %v: %v", sourceURL, destinationURL, err)
   184  	}
   185  	return err
   186  }
   187  
   188  //Archive archives supplied URL assets into zip writer
   189  func Archive(service Service, URL string, writer *zip.Writer) error {
   190  	memService := NewMemoryService()
   191  	var destURL = "mem:///dev/nul"
   192  	return Copy(service, URL, memService, destURL, nil, getArchiveCopyHandler(writer, destURL))
   193  }
   194  
   195  func getArchiveCopyHandlerWithFilter(archive *zip.Writer, parentURL string, predicate func(candidate Object) bool) CopyHandler {
   196  	return func(sourceObject Object, reader io.Reader, destinationService Service, destinationURL string) error {
   197  		if !predicate(sourceObject) {
   198  			return nil
   199  		}
   200  		var _, relativePath = toolbox.URLSplit(destinationURL)
   201  		if destinationURL != parentURL {
   202  			relativePath = strings.Replace(destinationURL, parentURL, "", 1)
   203  		}
   204  		header, err := zip.FileInfoHeader(sourceObject.FileInfo())
   205  		if err != nil {
   206  			return err
   207  		}
   208  		header.Method = zip.Store
   209  
   210  		if strings.HasPrefix(relativePath, "/") {
   211  			relativePath = string(relativePath[1:])
   212  		}
   213  		header.Name = relativePath
   214  		writer, err := archive.CreateHeader(header)
   215  		if err != nil {
   216  			return err
   217  		}
   218  		_, err = io.Copy(writer, reader)
   219  		return err
   220  	}
   221  }
   222  
   223  //Archive archives supplied URL assets into zip writer with supplied filter
   224  func ArchiveWithFilter(service Service, URL string, writer *zip.Writer, predicate func(candidate Object) bool) error {
   225  	memService := NewMemoryService()
   226  	var destURL = "mem:///dev/nul"
   227  	return Copy(service, URL, memService, destURL, nil, getArchiveCopyHandlerWithFilter(writer, destURL, predicate))
   228  }
   229  
   230  func getTarCopyHandler(archive *tar.Writer, destParentURL, parentURL string, dirs map[string]bool) CopyHandler {
   231  	if strings.HasSuffix(parentURL, "/") {
   232  		parentURL = string(parentURL[:len(parentURL)-2])
   233  	}
   234  	_, root := path.Split(destParentURL)
   235  	if root == "." {
   236  		root = ""
   237  	}
   238  	return func(sourceObject Object, reader io.Reader, destinationService Service, destinationURL string) error {
   239  		var _, relativePath = toolbox.URLSplit(destinationURL)
   240  		if destinationURL != parentURL {
   241  			relativePath = strings.Replace(destinationURL, parentURL, "", 1)
   242  		}
   243  
   244  		if strings.HasPrefix(relativePath, "/") {
   245  			relativePath = string(relativePath[1:])
   246  		}
   247  
   248  		relativePath = path.Join(root, relativePath)
   249  		parent, _ := path.Split(relativePath)
   250  
   251  		if parent != "" && !dirs[parent] {
   252  			tarHeader := &tar.Header{
   253  				Name:    parent,
   254  				Size:    int64(0),
   255  				Mode:    int64(sourceObject.FileInfo().Mode()),
   256  				ModTime: sourceObject.FileInfo().ModTime(),
   257  			}
   258  			if err := archive.WriteHeader(tarHeader); err != nil {
   259  				return fmt.Errorf(" unable to write tar header, %v", err)
   260  			}
   261  			dirs[parent] = true
   262  		}
   263  
   264  		contents := new(bytes.Buffer)
   265  		if _, err := io.Copy(contents, reader); err != nil {
   266  			return err
   267  		}
   268  		data := contents.Bytes()
   269  		tarHeader := &tar.Header{
   270  			Name:    relativePath,
   271  			Size:    int64(len(data)),
   272  			Mode:    int64(sourceObject.FileInfo().Mode()),
   273  			ModTime: sourceObject.FileInfo().ModTime(),
   274  		}
   275  		if err := archive.WriteHeader(tarHeader); err != nil {
   276  			return fmt.Errorf(" unable to write tar header, %v", err)
   277  		}
   278  		if _, err := archive.Write(data); err != nil {
   279  			return fmt.Errorf(" unable to write tar content, %v", err)
   280  		}
   281  		return nil
   282  	}
   283  }
   284  
   285  //Tar tar archives supplied URL assets into zip writer
   286  func Tar(service Service, URL string, writer *tar.Writer, includeOwnerDir bool) error {
   287  	memService := NewMemoryService()
   288  	var destURL = "mem:///dev/nul"
   289  	var dirs = make(map[string]bool)
   290  	ownerDir := ""
   291  	if includeOwnerDir {
   292  		ownerDir = URL
   293  	}
   294  	return Copy(service, URL, memService, destURL, nil, getTarCopyHandler(writer, ownerDir, destURL, dirs))
   295  }