github.com/cozy/cozy-stack@v0.0.0-20240327093429-939e4a21320e/model/move/cursor.go (about)

     1  package move
     2  
     3  import (
     4  	"fmt"
     5  	"sort"
     6  	"strings"
     7  
     8  	"github.com/cozy/cozy-stack/model/instance"
     9  	"github.com/cozy/cozy-stack/model/vfs"
    10  	"github.com/cozy/cozy-stack/pkg/consts"
    11  	"github.com/cozy/cozy-stack/pkg/couchdb"
    12  )
    13  
    14  // Cursor can be used to know which files must be included in a part.
    15  type Cursor struct {
    16  	Number  int
    17  	Doctype string
    18  	ID      string
    19  }
    20  
    21  // String returns a string representation of the cursor.
    22  func (c Cursor) String() string {
    23  	return fmt.Sprintf("%s/%s", c.Doctype, c.ID)
    24  }
    25  
    26  // ParseCursor checks that the given cursor is part of our pre-defined list of
    27  // cursors.
    28  func ParseCursor(exportDoc *ExportDoc, cursorStr string) (Cursor, error) {
    29  	if cursorStr == "" {
    30  		return Cursor{0, consts.Files, ""}, nil
    31  	}
    32  	for i, c := range exportDoc.PartsCursors {
    33  		if c == cursorStr {
    34  			return parseCursor(i+1, cursorStr)
    35  		}
    36  	}
    37  	return Cursor{}, ErrExportInvalidCursor
    38  }
    39  
    40  func parseCursor(number int, cursorStr string) (Cursor, error) {
    41  	parts := strings.SplitN(cursorStr, "/", 2)
    42  	if len(parts) != 2 {
    43  		return Cursor{}, ErrExportInvalidCursor
    44  	}
    45  	return Cursor{number, parts[0], parts[1]}, nil
    46  }
    47  
    48  func splitFiles(partsSize, remaining int64, sizesByID map[string]int64, doctype string) ([]string, int64) {
    49  	ids := make([]string, 0, len(sizesByID))
    50  	for id := range sizesByID {
    51  		ids = append(ids, id)
    52  	}
    53  	sort.Strings(ids)
    54  
    55  	cursors := make([]string, 0)
    56  	for _, id := range ids {
    57  		size := sizesByID[id]
    58  		if size > remaining && remaining != partsSize {
    59  			remaining = partsSize
    60  			cursor := Cursor{0, doctype, id}.String()
    61  			cursors = append(cursors, cursor)
    62  		}
    63  		remaining -= size
    64  	}
    65  
    66  	return cursors, remaining
    67  }
    68  
    69  func listFilesFromCursor(inst *instance.Instance, exportDoc *ExportDoc, start Cursor) ([]*vfs.FileDoc, error) {
    70  	if start.Doctype != consts.Files {
    71  		return []*vfs.FileDoc{}, nil
    72  	}
    73  
    74  	var end Cursor
    75  	if start.Number < len(exportDoc.PartsCursors) {
    76  		c, err := parseCursor(start.Number+1, exportDoc.PartsCursors[start.Number])
    77  		if err != nil {
    78  			return nil, err
    79  		}
    80  		end = c
    81  	}
    82  	if end.Doctype != consts.Files {
    83  		end = Cursor{len(exportDoc.PartsCursors), consts.Files, couchdb.MaxString}
    84  	}
    85  
    86  	var files []*vfs.FileDoc
    87  	req := couchdb.AllDocsRequest{
    88  		StartKeyDocID: start.ID,
    89  		EndKeyDocID:   end.ID,
    90  		Limit:         1000,
    91  	}
    92  	for {
    93  		var results []*vfs.FileDoc
    94  		if err := couchdb.GetAllDocs(inst, consts.Files, &req, &results); err != nil {
    95  			return nil, err
    96  		}
    97  		if len(results) == 0 {
    98  			break
    99  		}
   100  		for _, res := range results {
   101  			if res.DocID == end.ID {
   102  				return files, nil
   103  			}
   104  			if res.Type == consts.FileType { // Exclude the directories
   105  				files = append(files, res)
   106  			}
   107  		}
   108  		req.StartKeyDocID = results[len(results)-1].DocID
   109  		req.Skip = 1 // Do not fetch again the last file from this page
   110  	}
   111  
   112  	return files, nil
   113  }
   114  
   115  func listVersionsFromCursor(inst *instance.Instance, exportDoc *ExportDoc, start Cursor) ([]*vfs.Version, error) {
   116  	var end Cursor
   117  	if start.Number < len(exportDoc.PartsCursors) {
   118  		c, err := parseCursor(start.Number+1, exportDoc.PartsCursors[start.Number])
   119  		if err != nil {
   120  			return nil, err
   121  		}
   122  		end = c
   123  	}
   124  	if end.Doctype == consts.Files {
   125  		return []*vfs.Version{}, nil
   126  	} else if end.Doctype == "" {
   127  		end = Cursor{len(exportDoc.PartsCursors), consts.FilesVersions, couchdb.MaxString}
   128  	}
   129  
   130  	if start.Doctype != consts.FilesVersions {
   131  		start = Cursor{start.Number, consts.FilesVersions, ""}
   132  	}
   133  
   134  	var versions []*vfs.Version
   135  	req := couchdb.AllDocsRequest{
   136  		StartKeyDocID: start.ID,
   137  		EndKeyDocID:   end.ID,
   138  		Limit:         1000,
   139  	}
   140  	for {
   141  		var results []*vfs.Version
   142  		if err := couchdb.GetAllDocs(inst, consts.FilesVersions, &req, &results); err != nil {
   143  			return nil, err
   144  		}
   145  		if len(results) == 0 {
   146  			break
   147  		}
   148  		for _, res := range results {
   149  			if res.DocID == end.ID {
   150  				return versions, nil
   151  			}
   152  			versions = append(versions, res)
   153  		}
   154  		req.StartKeyDocID = results[len(results)-1].DocID
   155  		req.Skip = 1 // Do not fetch again the last file from this page
   156  	}
   157  
   158  	return versions, nil
   159  }