In keeping with other changes that migrate shared metadata to the onedrive/metadata pkg for exported access. --- #### Does this PR need a docs update or release note? - [x] ⛔ No #### Type of change - [x] 🧹 Tech Debt/Cleanup #### Issue(s) * #3135 #### Test Plan - [x] ⚡ Unit test - [x] 💚 E2E
987 lines
27 KiB
Go
987 lines
27 KiB
Go
package details
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/alcionai/clues"
|
|
"github.com/dustin/go-humanize"
|
|
"golang.org/x/exp/maps"
|
|
|
|
"github.com/alcionai/corso/src/cli/print"
|
|
"github.com/alcionai/corso/src/internal/common/dttm"
|
|
"github.com/alcionai/corso/src/internal/connector/onedrive/metadata"
|
|
"github.com/alcionai/corso/src/internal/version"
|
|
"github.com/alcionai/corso/src/pkg/path"
|
|
)
|
|
|
|
// LocationIDer provides access to location information but guarantees that it
|
|
// can also generate a unique location (among items in the same service but
|
|
// possibly across data types within the service) that can be used as a key in
|
|
// maps and other structures. The unique location may be different than
|
|
// InDetails, the location used in backup details.
|
|
type LocationIDer interface {
|
|
ID() *path.Builder
|
|
InDetails() *path.Builder
|
|
}
|
|
|
|
type uniqueLoc struct {
|
|
pb *path.Builder
|
|
prefixElems int
|
|
}
|
|
|
|
func (ul uniqueLoc) ID() *path.Builder {
|
|
return ul.pb
|
|
}
|
|
|
|
func (ul uniqueLoc) InDetails() *path.Builder {
|
|
return path.Builder{}.Append(ul.pb.Elements()[ul.prefixElems:]...)
|
|
}
|
|
|
|
// elementCount returns the number of non-prefix elements in the LocationIDer
|
|
// (i.e. the number of elements in the InDetails path.Builder).
|
|
func (ul uniqueLoc) elementCount() int {
|
|
res := len(ul.pb.Elements()) - ul.prefixElems
|
|
if res < 0 {
|
|
res = 0
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
func (ul *uniqueLoc) dir() {
|
|
if ul.elementCount() == 0 {
|
|
return
|
|
}
|
|
|
|
ul.pb = ul.pb.Dir()
|
|
}
|
|
|
|
// lastElem returns the unescaped last element in the location. If the location
|
|
// is empty returns an empty string.
|
|
func (ul uniqueLoc) lastElem() string {
|
|
if ul.elementCount() == 0 {
|
|
return ""
|
|
}
|
|
|
|
return ul.pb.LastElem()
|
|
}
|
|
|
|
// Having service-specific constructors can be kind of clunky, but in this case
|
|
// I think they'd be useful to ensure the proper args are used since this
|
|
// path.Builder is used as a key in some maps.
|
|
|
|
// NewExchangeLocationIDer builds a LocationIDer for the given category and
|
|
// folder path. The path denoted by the folders should be unique within the
|
|
// category.
|
|
func NewExchangeLocationIDer(
|
|
category path.CategoryType,
|
|
escapedFolders ...string,
|
|
) (uniqueLoc, error) {
|
|
if err := path.ValidateServiceAndCategory(path.ExchangeService, category); err != nil {
|
|
return uniqueLoc{}, clues.Wrap(err, "making exchange LocationIDer")
|
|
}
|
|
|
|
pb := path.Builder{}.Append(category.String()).Append(escapedFolders...)
|
|
|
|
return uniqueLoc{
|
|
pb: pb,
|
|
prefixElems: 1,
|
|
}, nil
|
|
}
|
|
|
|
// NewOneDriveLocationIDer builds a LocationIDer for the drive and folder path.
|
|
// The path denoted by the folders should be unique within the drive.
|
|
func NewOneDriveLocationIDer(
|
|
driveID string,
|
|
escapedFolders ...string,
|
|
) uniqueLoc {
|
|
pb := path.Builder{}.
|
|
Append(path.FilesCategory.String(), driveID).
|
|
Append(escapedFolders...)
|
|
|
|
return uniqueLoc{
|
|
pb: pb,
|
|
prefixElems: 2,
|
|
}
|
|
}
|
|
|
|
// NewSharePointLocationIDer builds a LocationIDer for the drive and folder
|
|
// path. The path denoted by the folders should be unique within the drive.
|
|
func NewSharePointLocationIDer(
|
|
driveID string,
|
|
escapedFolders ...string,
|
|
) uniqueLoc {
|
|
pb := path.Builder{}.
|
|
Append(path.LibrariesCategory.String(), driveID).
|
|
Append(escapedFolders...)
|
|
|
|
return uniqueLoc{
|
|
pb: pb,
|
|
prefixElems: 2,
|
|
}
|
|
}
|
|
|
|
// --------------------------------------------------------------------------------
|
|
// Model
|
|
// --------------------------------------------------------------------------------
|
|
|
|
// DetailsModel describes what was stored in a Backup
|
|
type DetailsModel struct {
|
|
Entries []Entry `json:"entries"`
|
|
}
|
|
|
|
// Print writes the DetailModel Entries to StdOut, in the format
|
|
// requested by the caller.
|
|
func (dm DetailsModel) PrintEntries(ctx context.Context) {
|
|
if print.DisplayJSONFormat() {
|
|
printJSON(ctx, dm)
|
|
} else {
|
|
printTable(ctx, dm)
|
|
}
|
|
}
|
|
|
|
func printTable(ctx context.Context, dm DetailsModel) {
|
|
perType := map[ItemType][]print.Printable{}
|
|
|
|
for _, de := range dm.Entries {
|
|
it := de.infoType()
|
|
ps, ok := perType[it]
|
|
|
|
if !ok {
|
|
ps = []print.Printable{}
|
|
}
|
|
|
|
perType[it] = append(ps, print.Printable(de))
|
|
}
|
|
|
|
for _, ps := range perType {
|
|
print.All(ctx, ps...)
|
|
}
|
|
}
|
|
|
|
func printJSON(ctx context.Context, dm DetailsModel) {
|
|
ents := []print.Printable{}
|
|
|
|
for _, ent := range dm.Entries {
|
|
ents = append(ents, print.Printable(ent))
|
|
}
|
|
|
|
print.All(ctx, ents...)
|
|
}
|
|
|
|
// Paths returns the list of Paths for non-folder and non-meta items extracted
|
|
// from the Entries slice.
|
|
func (dm DetailsModel) Paths() []string {
|
|
r := make([]string, 0, len(dm.Entries))
|
|
|
|
for _, ent := range dm.Entries {
|
|
if ent.Folder != nil || ent.isMetaFile() {
|
|
continue
|
|
}
|
|
|
|
r = append(r, ent.RepoRef)
|
|
}
|
|
|
|
return r
|
|
}
|
|
|
|
// Items returns a slice of *ItemInfo that does not contain any FolderInfo
|
|
// entries. Required because not all folders in the details are valid resource
|
|
// paths, and we want to slice out metadata.
|
|
func (dm DetailsModel) Items() []*Entry {
|
|
res := make([]*Entry, 0, len(dm.Entries))
|
|
|
|
for i := 0; i < len(dm.Entries); i++ {
|
|
ent := dm.Entries[i]
|
|
if ent.Folder != nil || ent.isMetaFile() {
|
|
continue
|
|
}
|
|
|
|
res = append(res, &ent)
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
// FilterMetaFiles returns a copy of the Details with all of the
|
|
// .meta files removed from the entries.
|
|
func (dm DetailsModel) FilterMetaFiles() DetailsModel {
|
|
d2 := DetailsModel{
|
|
Entries: []Entry{},
|
|
}
|
|
|
|
for _, ent := range dm.Entries {
|
|
if !ent.isMetaFile() {
|
|
d2.Entries = append(d2.Entries, ent)
|
|
}
|
|
}
|
|
|
|
return d2
|
|
}
|
|
|
|
// Check if a file is a metadata file. These are used to store
|
|
// additional data like permissions in case of OneDrive and are not to
|
|
// be treated as regular files.
|
|
func (de Entry) isMetaFile() bool {
|
|
// TODO: Add meta file filtering to SharePoint as well once we add
|
|
// meta files for SharePoint.
|
|
return de.ItemInfo.OneDrive != nil && de.ItemInfo.OneDrive.IsMeta
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Builder
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// Builder should be used to create a details model.
|
|
type Builder struct {
|
|
d Details
|
|
mu sync.Mutex `json:"-"`
|
|
knownFolders map[string]Entry `json:"-"`
|
|
}
|
|
|
|
func (b *Builder) Add(
|
|
repoRef path.Path,
|
|
locationRef *path.Builder,
|
|
updated bool,
|
|
info ItemInfo,
|
|
) error {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
|
|
entry, err := b.d.add(
|
|
repoRef,
|
|
locationRef,
|
|
updated,
|
|
info)
|
|
if err != nil {
|
|
return clues.Wrap(err, "adding entry to details")
|
|
}
|
|
|
|
if err := b.addFolderEntries(
|
|
repoRef.ToBuilder().Dir(),
|
|
locationRef,
|
|
entry,
|
|
); err != nil {
|
|
return clues.Wrap(err, "adding folder entries")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *Builder) addFolderEntries(
|
|
repoRef, locationRef *path.Builder,
|
|
entry Entry,
|
|
) error {
|
|
if len(repoRef.Elements()) < len(locationRef.Elements()) {
|
|
return clues.New("RepoRef shorter than LocationRef").
|
|
With("repo_ref", repoRef, "location_ref", locationRef)
|
|
}
|
|
|
|
if b.knownFolders == nil {
|
|
b.knownFolders = map[string]Entry{}
|
|
}
|
|
|
|
// Need a unique location because we want to have separate folders for
|
|
// different drives and categories even if there's duplicate folder names in
|
|
// them.
|
|
uniqueLoc, err := entry.uniqueLocation(locationRef)
|
|
if err != nil {
|
|
return clues.Wrap(err, "getting LocationIDer")
|
|
}
|
|
|
|
for uniqueLoc.elementCount() > 0 {
|
|
mapKey := uniqueLoc.ID().ShortRef()
|
|
|
|
name := uniqueLoc.lastElem()
|
|
if len(name) == 0 {
|
|
return clues.New("folder with no display name").
|
|
With("repo_ref", repoRef, "location_ref", uniqueLoc.InDetails())
|
|
}
|
|
|
|
shortRef := repoRef.ShortRef()
|
|
rr := repoRef.String()
|
|
|
|
// Get the parent of this entry to add as the LocationRef for the folder.
|
|
uniqueLoc.dir()
|
|
|
|
repoRef = repoRef.Dir()
|
|
parentRef := repoRef.ShortRef()
|
|
|
|
folder, ok := b.knownFolders[mapKey]
|
|
if !ok {
|
|
loc := uniqueLoc.InDetails().String()
|
|
|
|
folder = Entry{
|
|
RepoRef: rr,
|
|
ShortRef: shortRef,
|
|
ParentRef: parentRef,
|
|
LocationRef: loc,
|
|
ItemInfo: ItemInfo{
|
|
Folder: &FolderInfo{
|
|
ItemType: FolderItem,
|
|
// TODO(ashmrtn): Use the item type returned by the entry once
|
|
// SharePoint properly sets it.
|
|
DisplayName: name,
|
|
},
|
|
},
|
|
}
|
|
|
|
if err := entry.updateFolder(folder.Folder); err != nil {
|
|
return clues.Wrap(err, "adding folder").
|
|
With("parent_repo_ref", repoRef, "location_ref", loc)
|
|
}
|
|
}
|
|
|
|
folder.Folder.Size += entry.size()
|
|
folder.Updated = folder.Updated || entry.Updated
|
|
|
|
itemModified := entry.Modified()
|
|
if folder.Folder.Modified.Before(itemModified) {
|
|
folder.Folder.Modified = itemModified
|
|
}
|
|
|
|
// Always update the map because we're storing structs not pointers to
|
|
// structs.
|
|
b.knownFolders[mapKey] = folder
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *Builder) Details() *Details {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
|
|
// Write the cached folder entries to details
|
|
b.d.Entries = append(b.d.Entries, maps.Values(b.knownFolders)...)
|
|
|
|
return &b.d
|
|
}
|
|
|
|
// --------------------------------------------------------------------------------
|
|
// Details
|
|
// --------------------------------------------------------------------------------
|
|
|
|
// Details augments the core with a mutex for processing.
|
|
// Should be sliced back to d.DetailsModel for storage and
|
|
// printing.
|
|
type Details struct {
|
|
DetailsModel
|
|
}
|
|
|
|
func (d *Details) add(
|
|
repoRef path.Path,
|
|
locationRef *path.Builder,
|
|
updated bool,
|
|
info ItemInfo,
|
|
) (Entry, error) {
|
|
if locationRef == nil {
|
|
return Entry{}, clues.New("nil LocationRef").With("repo_ref", repoRef)
|
|
}
|
|
|
|
entry := Entry{
|
|
RepoRef: repoRef.String(),
|
|
ShortRef: repoRef.ShortRef(),
|
|
ParentRef: repoRef.ToBuilder().Dir().ShortRef(),
|
|
LocationRef: locationRef.String(),
|
|
ItemRef: repoRef.Item(),
|
|
Updated: updated,
|
|
ItemInfo: info,
|
|
}
|
|
|
|
// Use the item name and the path for the ShortRef. This ensures that renames
|
|
// within a directory generate unique ShortRefs.
|
|
if info.infoType() == OneDriveItem || info.infoType() == SharePointLibrary {
|
|
if info.OneDrive == nil && info.SharePoint == nil {
|
|
return entry, clues.New("item is not SharePoint or OneDrive type")
|
|
}
|
|
|
|
filename := ""
|
|
if info.OneDrive != nil {
|
|
filename = info.OneDrive.ItemName
|
|
} else if info.SharePoint != nil {
|
|
filename = info.SharePoint.ItemName
|
|
}
|
|
|
|
// Make the new path contain all display names and then the M365 item ID.
|
|
// This ensures the path will be unique, thus ensuring the ShortRef will be
|
|
// unique.
|
|
//
|
|
// If we appended the file's display name to the path then it's possible
|
|
// for a folder in the parent directory to have the same display name as the
|
|
// M365 ID of this file and also have a subfolder in the folder with a
|
|
// display name that matches the file's display name. That would result in
|
|
// duplicate ShortRefs, which we can't allow.
|
|
elements := repoRef.Elements()
|
|
elements = append(elements[:len(elements)-1], filename, repoRef.Item())
|
|
entry.ShortRef = path.Builder{}.Append(elements...).ShortRef()
|
|
|
|
// clean metadata suffixes from item refs
|
|
entry.ItemRef = withoutMetadataSuffix(entry.ItemRef)
|
|
}
|
|
|
|
d.Entries = append(d.Entries, entry)
|
|
|
|
return entry, nil
|
|
}
|
|
|
|
// Marshal complies with the marshaller interface in streamStore.
|
|
func (d *Details) Marshal() ([]byte, error) {
|
|
return json.Marshal(d)
|
|
}
|
|
|
|
// UnmarshalTo produces a func that complies with the unmarshaller type in streamStore.
|
|
func UnmarshalTo(d *Details) func(io.ReadCloser) error {
|
|
return func(rc io.ReadCloser) error {
|
|
return json.NewDecoder(rc).Decode(d)
|
|
}
|
|
}
|
|
|
|
// remove metadata file suffixes from the string.
|
|
// assumes only one suffix is applied to any given id.
|
|
func withoutMetadataSuffix(id string) string {
|
|
id = strings.TrimSuffix(id, metadata.DirMetaFileSuffix)
|
|
id = strings.TrimSuffix(id, metadata.MetaFileSuffix)
|
|
id = strings.TrimSuffix(id, metadata.DataFileSuffix)
|
|
|
|
return id
|
|
}
|
|
|
|
// --------------------------------------------------------------------------------
|
|
// Entry
|
|
// --------------------------------------------------------------------------------
|
|
|
|
// Entry describes a single item stored in a Backup
|
|
type Entry struct {
|
|
// RepoRef is the full storage path of the item in Kopia
|
|
RepoRef string `json:"repoRef"`
|
|
ShortRef string `json:"shortRef"`
|
|
ParentRef string `json:"parentRef,omitempty"`
|
|
|
|
// LocationRef contains the logical path structure by its human-readable
|
|
// display names. IE: If an item is located at "/Inbox/Important", we
|
|
// hold that string in the LocationRef, while the actual IDs of each
|
|
// container are used for the RepoRef.
|
|
// LocationRef only holds the container values, and does not include
|
|
// the metadata prefixes (tenant, service, owner, etc) found in the
|
|
// repoRef.
|
|
// Currently only implemented for Exchange Calendars.
|
|
LocationRef string `json:"locationRef,omitempty"`
|
|
|
|
// ItemRef contains the stable id of the item itself. ItemRef is not
|
|
// guaranteed to be unique within a repository. Uniqueness guarantees
|
|
// maximally inherit from the source item. Eg: Entries for m365 mail items
|
|
// are only as unique as m365 mail item IDs themselves.
|
|
ItemRef string `json:"itemRef,omitempty"`
|
|
|
|
// Indicates the item was added or updated in this backup
|
|
// Always `true` for full backups
|
|
Updated bool `json:"updated"`
|
|
|
|
ItemInfo
|
|
}
|
|
|
|
// ToLocationIDer takes a backup version and produces the unique location for
|
|
// this entry if possible. Reasons it may not be possible to produce the unique
|
|
// location include an unsupported backup version or missing information.
|
|
func (de Entry) ToLocationIDer(backupVersion int) (LocationIDer, error) {
|
|
if len(de.LocationRef) > 0 {
|
|
baseLoc, err := path.Builder{}.SplitUnescapeAppend(de.LocationRef)
|
|
if err != nil {
|
|
return nil, clues.Wrap(err, "parsing base location info").
|
|
With("location_ref", de.LocationRef)
|
|
}
|
|
|
|
// Individual services may add additional info to the base and return that.
|
|
return de.ItemInfo.uniqueLocation(baseLoc)
|
|
}
|
|
|
|
if backupVersion >= version.OneDrive7LocationRef ||
|
|
(de.ItemInfo.infoType() != OneDriveItem &&
|
|
de.ItemInfo.infoType() != SharePointLibrary) {
|
|
return nil, clues.New("no previous location for entry")
|
|
}
|
|
|
|
// This is a little hacky, but we only want to try to extract the old
|
|
// location if it's OneDrive or SharePoint libraries and it's known to
|
|
// be an older backup version.
|
|
//
|
|
// TODO(ashmrtn): Remove this code once OneDrive/SharePoint libraries
|
|
// LocationRef code has been out long enough that all delta tokens for
|
|
// previous backup versions will have expired. At that point, either
|
|
// we'll do a full backup (token expired, no newer backups) or have a
|
|
// backup of a higher version with the information we need.
|
|
rr, err := path.FromDataLayerPath(de.RepoRef, true)
|
|
if err != nil {
|
|
return nil, clues.Wrap(err, "getting item RepoRef")
|
|
}
|
|
|
|
p, err := path.ToDrivePath(rr)
|
|
if err != nil {
|
|
return nil, clues.New("converting RepoRef to drive path")
|
|
}
|
|
|
|
baseLoc := path.Builder{}.Append(p.Root).Append(p.Folders...)
|
|
|
|
// Individual services may add additional info to the base and return that.
|
|
return de.ItemInfo.uniqueLocation(baseLoc)
|
|
}
|
|
|
|
// --------------------------------------------------------------------------------
|
|
// CLI Output
|
|
// --------------------------------------------------------------------------------
|
|
|
|
// interface compliance checks
|
|
var _ print.Printable = &Entry{}
|
|
|
|
// MinimumPrintable DetailsEntries is a passthrough func, because no
|
|
// reduction is needed for the json output.
|
|
func (de Entry) MinimumPrintable() any {
|
|
return de
|
|
}
|
|
|
|
// Headers returns the human-readable names of properties in a DetailsEntry
|
|
// for printing out to a terminal in a columnar display.
|
|
func (de Entry) Headers() []string {
|
|
hs := []string{"ID"}
|
|
|
|
if de.ItemInfo.Folder != nil {
|
|
hs = append(hs, de.ItemInfo.Folder.Headers()...)
|
|
}
|
|
|
|
if de.ItemInfo.Exchange != nil {
|
|
hs = append(hs, de.ItemInfo.Exchange.Headers()...)
|
|
}
|
|
|
|
if de.ItemInfo.SharePoint != nil {
|
|
hs = append(hs, de.ItemInfo.SharePoint.Headers()...)
|
|
}
|
|
|
|
if de.ItemInfo.OneDrive != nil {
|
|
hs = append(hs, de.ItemInfo.OneDrive.Headers()...)
|
|
}
|
|
|
|
return hs
|
|
}
|
|
|
|
// Values returns the values matching the Headers list.
|
|
func (de Entry) Values() []string {
|
|
vs := []string{de.ShortRef}
|
|
|
|
if de.ItemInfo.Folder != nil {
|
|
vs = append(vs, de.ItemInfo.Folder.Values()...)
|
|
}
|
|
|
|
if de.ItemInfo.Exchange != nil {
|
|
vs = append(vs, de.ItemInfo.Exchange.Values()...)
|
|
}
|
|
|
|
if de.ItemInfo.SharePoint != nil {
|
|
vs = append(vs, de.ItemInfo.SharePoint.Values()...)
|
|
}
|
|
|
|
if de.ItemInfo.OneDrive != nil {
|
|
vs = append(vs, de.ItemInfo.OneDrive.Values()...)
|
|
}
|
|
|
|
return vs
|
|
}
|
|
|
|
type ItemType int
|
|
|
|
// ItemTypes are enumerated by service (hundredth digit) and data type (ones digit).
|
|
// Ex: exchange is 00x where x is the data type. Sharepoint is 10x, and etc.
|
|
// Every item info struct should get its own hundredth enumeration entry.
|
|
// Every item category for that service should get its own entry (even if differences
|
|
// between types aren't apparent on initial implementation, this future-proofs
|
|
// against breaking changes).
|
|
// Entries should not be rearranged.
|
|
// Additionally, any itemType directly assigned a number should not be altered.
|
|
// This applies to OneDriveItem and FolderItem
|
|
const (
|
|
UnknownType ItemType = iota // 0, global unknown value
|
|
|
|
// Exchange (00x)
|
|
ExchangeContact
|
|
ExchangeEvent
|
|
ExchangeMail
|
|
// SharePoint (10x)
|
|
SharePointLibrary ItemType = iota + 97 // 100
|
|
SharePointList // 101...
|
|
SharePointPage
|
|
|
|
// OneDrive (20x)
|
|
OneDriveItem ItemType = 205
|
|
|
|
// Folder Management(30x)
|
|
FolderItem ItemType = 306
|
|
)
|
|
|
|
func UpdateItem(item *ItemInfo, newLocPath *path.Builder) {
|
|
// Only OneDrive and SharePoint have information about parent folders
|
|
// contained in them.
|
|
var updatePath func(newLocPath *path.Builder)
|
|
|
|
switch item.infoType() {
|
|
case ExchangeContact, ExchangeEvent, ExchangeMail:
|
|
updatePath = item.Exchange.UpdateParentPath
|
|
case SharePointLibrary:
|
|
updatePath = item.SharePoint.UpdateParentPath
|
|
case OneDriveItem:
|
|
updatePath = item.OneDrive.UpdateParentPath
|
|
default:
|
|
return
|
|
}
|
|
|
|
updatePath(newLocPath)
|
|
}
|
|
|
|
// ItemInfo is a oneOf that contains service specific
|
|
// information about the item it tracks
|
|
type ItemInfo struct {
|
|
Folder *FolderInfo `json:"folder,omitempty"`
|
|
Exchange *ExchangeInfo `json:"exchange,omitempty"`
|
|
SharePoint *SharePointInfo `json:"sharePoint,omitempty"`
|
|
OneDrive *OneDriveInfo `json:"oneDrive,omitempty"`
|
|
}
|
|
|
|
// typedInfo should get embedded in each sesrvice type to track
|
|
// the type of item it stores for multi-item service support.
|
|
|
|
// infoType provides internal categorization for collecting like-typed ItemInfos.
|
|
// It should return the most granular value type (ex: "event" for an exchange
|
|
// calendar event).
|
|
func (i ItemInfo) infoType() ItemType {
|
|
switch {
|
|
case i.Folder != nil:
|
|
return i.Folder.ItemType
|
|
|
|
case i.Exchange != nil:
|
|
return i.Exchange.ItemType
|
|
|
|
case i.SharePoint != nil:
|
|
return i.SharePoint.ItemType
|
|
|
|
case i.OneDrive != nil:
|
|
return i.OneDrive.ItemType
|
|
}
|
|
|
|
return UnknownType
|
|
}
|
|
|
|
func (i ItemInfo) size() int64 {
|
|
switch {
|
|
case i.Exchange != nil:
|
|
return i.Exchange.Size
|
|
|
|
case i.OneDrive != nil:
|
|
return i.OneDrive.Size
|
|
|
|
case i.SharePoint != nil:
|
|
return i.SharePoint.Size
|
|
|
|
case i.Folder != nil:
|
|
return i.Folder.Size
|
|
}
|
|
|
|
return 0
|
|
}
|
|
|
|
func (i ItemInfo) Modified() time.Time {
|
|
switch {
|
|
case i.Exchange != nil:
|
|
return i.Exchange.Modified
|
|
|
|
case i.OneDrive != nil:
|
|
return i.OneDrive.Modified
|
|
|
|
case i.SharePoint != nil:
|
|
return i.SharePoint.Modified
|
|
|
|
case i.Folder != nil:
|
|
return i.Folder.Modified
|
|
}
|
|
|
|
return time.Time{}
|
|
}
|
|
|
|
func (i ItemInfo) uniqueLocation(baseLoc *path.Builder) (*uniqueLoc, error) {
|
|
switch {
|
|
case i.Exchange != nil:
|
|
return i.Exchange.uniqueLocation(baseLoc)
|
|
|
|
case i.OneDrive != nil:
|
|
return i.OneDrive.uniqueLocation(baseLoc)
|
|
|
|
case i.SharePoint != nil:
|
|
return i.SharePoint.uniqueLocation(baseLoc)
|
|
|
|
default:
|
|
return nil, clues.New("unsupported type")
|
|
}
|
|
}
|
|
|
|
func (i ItemInfo) updateFolder(f *FolderInfo) error {
|
|
switch {
|
|
case i.Exchange != nil:
|
|
return i.Exchange.updateFolder(f)
|
|
|
|
case i.OneDrive != nil:
|
|
return i.OneDrive.updateFolder(f)
|
|
|
|
case i.SharePoint != nil:
|
|
return i.SharePoint.updateFolder(f)
|
|
|
|
default:
|
|
return clues.New("unsupported type")
|
|
}
|
|
}
|
|
|
|
type FolderInfo struct {
|
|
ItemType ItemType `json:"itemType,omitempty"`
|
|
DisplayName string `json:"displayName"`
|
|
Modified time.Time `json:"modified,omitempty"`
|
|
Size int64 `json:"size,omitempty"`
|
|
DataType ItemType `json:"dataType,omitempty"`
|
|
DriveName string `json:"driveName,omitempty"`
|
|
DriveID string `json:"driveID,omitempty"`
|
|
}
|
|
|
|
func (i FolderInfo) Headers() []string {
|
|
return []string{"Display Name"}
|
|
}
|
|
|
|
func (i FolderInfo) Values() []string {
|
|
return []string{i.DisplayName}
|
|
}
|
|
|
|
// ExchangeInfo describes an exchange item
|
|
type ExchangeInfo struct {
|
|
ItemType ItemType `json:"itemType,omitempty"`
|
|
Sender string `json:"sender,omitempty"`
|
|
Subject string `json:"subject,omitempty"`
|
|
Recipient []string `json:"recipient,omitempty"`
|
|
ParentPath string `json:"parentPath,omitempty"`
|
|
Received time.Time `json:"received,omitempty"`
|
|
EventStart time.Time `json:"eventStart,omitempty"`
|
|
EventEnd time.Time `json:"eventEnd,omitempty"`
|
|
Organizer string `json:"organizer,omitempty"`
|
|
ContactName string `json:"contactName,omitempty"`
|
|
EventRecurs bool `json:"eventRecurs,omitempty"`
|
|
Created time.Time `json:"created,omitempty"`
|
|
Modified time.Time `json:"modified,omitempty"`
|
|
Size int64 `json:"size,omitempty"`
|
|
}
|
|
|
|
// Headers returns the human-readable names of properties in an ExchangeInfo
|
|
// for printing out to a terminal in a columnar display.
|
|
func (i ExchangeInfo) Headers() []string {
|
|
switch i.ItemType {
|
|
case ExchangeEvent:
|
|
return []string{"Organizer", "Subject", "Starts", "Ends", "Recurring"}
|
|
|
|
case ExchangeContact:
|
|
return []string{"Contact Name"}
|
|
|
|
case ExchangeMail:
|
|
return []string{"Sender", "Folder", "Subject", "Received"}
|
|
}
|
|
|
|
return []string{}
|
|
}
|
|
|
|
// Values returns the values matching the Headers list for printing
|
|
// out to a terminal in a columnar display.
|
|
func (i ExchangeInfo) Values() []string {
|
|
switch i.ItemType {
|
|
case ExchangeEvent:
|
|
return []string{
|
|
i.Organizer,
|
|
i.Subject,
|
|
dttm.FormatToTabularDisplay(i.EventStart),
|
|
dttm.FormatToTabularDisplay(i.EventEnd),
|
|
strconv.FormatBool(i.EventRecurs),
|
|
}
|
|
|
|
case ExchangeContact:
|
|
return []string{i.ContactName}
|
|
|
|
case ExchangeMail:
|
|
return []string{
|
|
i.Sender, i.ParentPath, i.Subject,
|
|
dttm.FormatToTabularDisplay(i.Received),
|
|
}
|
|
}
|
|
|
|
return []string{}
|
|
}
|
|
|
|
func (i *ExchangeInfo) UpdateParentPath(newLocPath *path.Builder) {
|
|
i.ParentPath = newLocPath.String()
|
|
}
|
|
|
|
func (i *ExchangeInfo) uniqueLocation(baseLoc *path.Builder) (*uniqueLoc, error) {
|
|
var category path.CategoryType
|
|
|
|
switch i.ItemType {
|
|
case ExchangeEvent:
|
|
category = path.EventsCategory
|
|
case ExchangeContact:
|
|
category = path.ContactsCategory
|
|
case ExchangeMail:
|
|
category = path.EmailCategory
|
|
}
|
|
|
|
loc, err := NewExchangeLocationIDer(category, baseLoc.Elements()...)
|
|
|
|
return &loc, err
|
|
}
|
|
|
|
func (i *ExchangeInfo) updateFolder(f *FolderInfo) error {
|
|
// Use a switch instead of a rather large if-statement. Just make sure it's an
|
|
// Exchange type. If it's not return an error.
|
|
switch i.ItemType {
|
|
case ExchangeContact, ExchangeEvent, ExchangeMail:
|
|
default:
|
|
return clues.New("unsupported non-Exchange ItemType").
|
|
With("item_type", i.ItemType)
|
|
}
|
|
|
|
f.DataType = i.ItemType
|
|
|
|
return nil
|
|
}
|
|
|
|
// SharePointInfo describes a sharepoint item
|
|
type SharePointInfo struct {
|
|
Created time.Time `json:"created,omitempty"`
|
|
DriveName string `json:"driveName,omitempty"`
|
|
DriveID string `json:"driveID,omitempty"`
|
|
ItemName string `json:"itemName,omitempty"`
|
|
ItemType ItemType `json:"itemType,omitempty"`
|
|
Modified time.Time `josn:"modified,omitempty"`
|
|
Owner string `json:"owner,omitempty"`
|
|
ParentPath string `json:"parentPath,omitempty"`
|
|
Size int64 `json:"size,omitempty"`
|
|
WebURL string `json:"webUrl,omitempty"`
|
|
}
|
|
|
|
// Headers returns the human-readable names of properties in a SharePointInfo
|
|
// for printing out to a terminal in a columnar display.
|
|
func (i SharePointInfo) Headers() []string {
|
|
return []string{"ItemName", "Library", "ParentPath", "Size", "Owner", "Created", "Modified"}
|
|
}
|
|
|
|
// Values returns the values matching the Headers list for printing
|
|
// out to a terminal in a columnar display.
|
|
func (i SharePointInfo) Values() []string {
|
|
return []string{
|
|
i.ItemName,
|
|
i.DriveName,
|
|
i.ParentPath,
|
|
humanize.Bytes(uint64(i.Size)),
|
|
i.Owner,
|
|
dttm.FormatToTabularDisplay(i.Created),
|
|
dttm.FormatToTabularDisplay(i.Modified),
|
|
}
|
|
}
|
|
|
|
func (i *SharePointInfo) UpdateParentPath(newLocPath *path.Builder) {
|
|
i.ParentPath = newLocPath.PopFront().String()
|
|
}
|
|
|
|
func (i *SharePointInfo) uniqueLocation(baseLoc *path.Builder) (*uniqueLoc, error) {
|
|
if len(i.DriveID) == 0 {
|
|
return nil, clues.New("empty drive ID")
|
|
}
|
|
|
|
loc := NewSharePointLocationIDer(i.DriveID, baseLoc.Elements()...)
|
|
|
|
return &loc, nil
|
|
}
|
|
|
|
func (i *SharePointInfo) updateFolder(f *FolderInfo) error {
|
|
// TODO(ashmrtn): Change to just SharePointLibrary when the code that
|
|
// generates the item type is fixed.
|
|
if i.ItemType == OneDriveItem || i.ItemType == SharePointLibrary {
|
|
return updateFolderWithinDrive(SharePointLibrary, i.DriveName, i.DriveID, f)
|
|
}
|
|
|
|
return clues.New("unsupported non-SharePoint ItemType").With("item_type", i.ItemType)
|
|
}
|
|
|
|
// OneDriveInfo describes a oneDrive item
|
|
type OneDriveInfo struct {
|
|
Created time.Time `json:"created,omitempty"`
|
|
DriveID string `json:"driveID,omitempty"`
|
|
DriveName string `json:"driveName,omitempty"`
|
|
IsMeta bool `json:"isMeta,omitempty"`
|
|
ItemName string `json:"itemName,omitempty"`
|
|
ItemType ItemType `json:"itemType,omitempty"`
|
|
Modified time.Time `json:"modified,omitempty"`
|
|
Owner string `json:"owner,omitempty"`
|
|
ParentPath string `json:"parentPath"`
|
|
Size int64 `json:"size,omitempty"`
|
|
}
|
|
|
|
// Headers returns the human-readable names of properties in a OneDriveInfo
|
|
// for printing out to a terminal in a columnar display.
|
|
func (i OneDriveInfo) Headers() []string {
|
|
return []string{"ItemName", "ParentPath", "Size", "Owner", "Created", "Modified"}
|
|
}
|
|
|
|
// Values returns the values matching the Headers list for printing
|
|
// out to a terminal in a columnar display.
|
|
func (i OneDriveInfo) Values() []string {
|
|
return []string{
|
|
i.ItemName,
|
|
i.ParentPath,
|
|
humanize.Bytes(uint64(i.Size)),
|
|
i.Owner,
|
|
dttm.FormatToTabularDisplay(i.Created),
|
|
dttm.FormatToTabularDisplay(i.Modified),
|
|
}
|
|
}
|
|
|
|
func (i *OneDriveInfo) UpdateParentPath(newLocPath *path.Builder) {
|
|
i.ParentPath = newLocPath.PopFront().String()
|
|
}
|
|
|
|
func (i *OneDriveInfo) uniqueLocation(baseLoc *path.Builder) (*uniqueLoc, error) {
|
|
if len(i.DriveID) == 0 {
|
|
return nil, clues.New("empty drive ID")
|
|
}
|
|
|
|
loc := NewOneDriveLocationIDer(i.DriveID, baseLoc.Elements()...)
|
|
|
|
return &loc, nil
|
|
}
|
|
|
|
func (i *OneDriveInfo) updateFolder(f *FolderInfo) error {
|
|
return updateFolderWithinDrive(OneDriveItem, i.DriveName, i.DriveID, f)
|
|
}
|
|
|
|
func updateFolderWithinDrive(
|
|
t ItemType,
|
|
driveName, driveID string,
|
|
f *FolderInfo,
|
|
) error {
|
|
if len(driveName) == 0 {
|
|
return clues.New("empty drive name")
|
|
} else if len(driveID) == 0 {
|
|
return clues.New("empty drive ID")
|
|
}
|
|
|
|
f.DriveName = driveName
|
|
f.DriveID = driveID
|
|
f.DataType = t
|
|
|
|
return nil
|
|
}
|