small paths code rearrangement (#4007)

small cleanup in paths, primarily splitting files
so that file contents are more clearly owned, which should be a little better for readability and code placement.

Also renames `ServicePrefix` to `BuildPrefix` in
anticipation of multi-service prefixes.

no logic changes, just movement/renaming.

---

#### Does this PR need a docs update or release note?

- [x]  No

#### Type of change

- [x] 🧹 Tech Debt/Cleanup

#### Issue(s)

* #3993

#### Test Plan

- [x]  Unit test
- [x] 💚 E2E
This commit is contained in:
Keepers 2023-08-11 17:02:45 -06:00 committed by GitHub
parent 19bf0fdf7e
commit 1f22b94fc1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 946 additions and 891 deletions

View File

@ -70,7 +70,7 @@ func (r reason) Category() path.CategoryType {
}
func (r reason) SubtreePath() (path.Path, error) {
p, err := path.ServicePrefix(
p, err := path.BuildPrefix(
r.Tenant(),
r.ProtectedResource(),
r.Service(),

View File

@ -2865,16 +2865,16 @@ func (suite *HierarchyBuilderUnitSuite) TestBuildDirectoryTreeSelectsMigrateSubt
migratedUser = "user_migrate"
)
oldPrefixPathEmail, err := path.ServicePrefix(testTenant, testUser, path.ExchangeService, path.EmailCategory)
oldPrefixPathEmail, err := path.BuildPrefix(testTenant, testUser, path.ExchangeService, path.EmailCategory)
require.NoError(t, err, clues.ToCore(err))
newPrefixPathEmail, err := path.ServicePrefix(testTenant, migratedUser, path.ExchangeService, path.EmailCategory)
newPrefixPathEmail, err := path.BuildPrefix(testTenant, migratedUser, path.ExchangeService, path.EmailCategory)
require.NoError(t, err, clues.ToCore(err))
oldPrefixPathCont, err := path.ServicePrefix(testTenant, testUser, path.ExchangeService, path.ContactsCategory)
oldPrefixPathCont, err := path.BuildPrefix(testTenant, testUser, path.ExchangeService, path.ContactsCategory)
require.NoError(t, err, clues.ToCore(err))
newPrefixPathCont, err := path.ServicePrefix(testTenant, migratedUser, path.ExchangeService, path.ContactsCategory)
newPrefixPathCont, err := path.BuildPrefix(testTenant, migratedUser, path.ExchangeService, path.ContactsCategory)
require.NoError(t, err, clues.ToCore(err))
var (

View File

@ -79,7 +79,7 @@ func BaseCollections(
for cat := range categories {
ictx := clues.Add(ctx, "base_service", service, "base_category", cat)
full, err := path.ServicePrefix(tenant, rOwner, service, cat)
full, err := path.BuildPrefix(tenant, rOwner, service, cat)
if err != nil {
// Shouldn't happen.
err = clues.Wrap(err, "making path").WithClues(ictx)

View File

@ -24,10 +24,10 @@ func (suite *CollectionsUnitSuite) TestNewPrefixCollection() {
serv := path.OneDriveService
cat := path.FilesCategory
p1, err := path.ServicePrefix("t", "ro1", serv, cat)
p1, err := path.BuildPrefix("t", "ro1", serv, cat)
require.NoError(t, err, clues.ToCore(err))
p2, err := path.ServicePrefix("t", "ro2", serv, cat)
p2, err := path.BuildPrefix("t", "ro2", serv, cat)
require.NoError(t, err, clues.ToCore(err))
items, err := path.Build("t", "ro", serv, cat, true, "fld", "itm")

View File

@ -109,7 +109,7 @@ func migrationCollections(
// unlike exchange, which enumerates all folders on every
// backup, onedrive needs to force the owner PN -> ID migration
mc, err := path.ServicePrefix(
mc, err := path.BuildPrefix(
tenant,
bpc.ProtectedResource.ID(),
path.OneDriveService,
@ -118,7 +118,7 @@ func migrationCollections(
return nil, clues.Wrap(err, "creating user id migration path")
}
mpc, err := path.ServicePrefix(
mpc, err := path.BuildPrefix(
tenant,
bpc.ProtectedResource.Name(),
path.OneDriveService,

View File

@ -42,7 +42,7 @@ func locationRef(
func basicLocationPath(repoRef path.Path, locRef *path.Builder) (path.Path, error) {
if len(locRef.Elements()) == 0 {
res, err := path.ServicePrefix(
res, err := path.BuildPrefix(
repoRef.Tenant(),
repoRef.ResourceOwner(),
repoRef.Service(),

View File

@ -347,7 +347,7 @@ func testExchangeContinuousBackups(suite *ExchangeBackupIntgSuite, toggles contr
// },
}
rrPfx, err := path.ServicePrefix(acct.ID(), uidn.ID(), service, path.EmailCategory)
rrPfx, err := path.BuildPrefix(acct.ID(), uidn.ID(), service, path.EmailCategory)
require.NoError(t, err, clues.ToCore(err))
// strip the category from the prefix; we primarily want the tenant and resource owner.

View File

@ -213,7 +213,7 @@ func runDriveIncrementalTest(
}
)
rrPfx, err := path.ServicePrefix(atid, roidn.ID(), service, category)
rrPfx, err := path.BuildPrefix(atid, roidn.ID(), service, category)
require.NoError(t, err, clues.ToCore(err))
// strip the category from the prefix; we primarily want the tenant and resource owner.

379
src/pkg/path/builder.go Normal file
View File

@ -0,0 +1,379 @@
package path
import (
"bytes"
"crypto/sha256"
"fmt"
"github.com/alcionai/clues"
)
// interface compliance required for handling PII
var (
_ clues.Concealer = &Builder{}
_ fmt.Stringer = &Builder{}
)
// Builder is a simple path representation that only tracks path elements. It
// can join, escape, and unescape elements. Higher-level packages are expected
// to wrap this struct to build resource-specific contexts (e.x. an
// ExchangeMailPath).
// Resource-specific paths allow access to more information like segments in the
// path. Builders that are turned into resource paths later on do not need to
// manually add prefixes for items that normally appear in the data layer (ex.
// tenant ID, service, user ID, etc).
type Builder struct {
// Unescaped version of elements.
elements Elements
}
// Append creates a copy of this Builder and adds the given elements them to the
// end of the new Builder. Elements are added in the order they are passed.
func (pb Builder) Append(elements ...string) *Builder {
res := &Builder{elements: make([]string, len(pb.elements))}
copy(res.elements, pb.elements)
// Unescaped elements can't fail validation.
//nolint:errcheck
res.appendElements(false, elements)
return res
}
func (pb *Builder) appendElements(escaped bool, elements []string) error {
for _, e := range elements {
if len(e) == 0 {
continue
}
tmp := e
if escaped {
tmp = TrimTrailingSlash(tmp)
// If tmp was just the path separator then it will be empty now.
if len(tmp) == 0 {
continue
}
if err := validateEscapedElement(tmp); err != nil {
return err
}
tmp = unescape(tmp)
}
pb.elements = append(pb.elements, tmp)
}
return nil
}
// UnescapeAndAppend creates a copy of this Builder and adds one or more already
// escaped path elements to the end of the new Builder. Elements are added in
// the order they are passed.
func (pb Builder) UnescapeAndAppend(elements ...string) (*Builder, error) {
res := &Builder{elements: make([]string, 0, len(pb.elements))}
copy(res.elements, pb.elements)
if err := res.appendElements(true, elements); err != nil {
return nil, err
}
return res, nil
}
// SplitUnescapeAppend takes in an escaped string representing a directory
// path, splits the string, and appends it to the current builder.
func (pb Builder) SplitUnescapeAppend(s string) (*Builder, error) {
elems := Split(TrimTrailingSlash(s))
return pb.UnescapeAndAppend(elems...)
}
func (pb Builder) PopFront() *Builder {
if len(pb.elements) <= 1 {
return &Builder{}
}
elements := make([]string, len(pb.elements)-1)
copy(elements, pb.elements[1:])
return &Builder{
elements: elements,
}
}
// Dir removes the last element from the builder.
func (pb Builder) Dir() *Builder {
if len(pb.elements) <= 1 {
return &Builder{}
}
return &Builder{
// Safe to use the same elements because Builders are immutable.
elements: pb.elements[:len(pb.elements)-1],
}
}
// HeadElem returns the first element in the Builder.
func (pb Builder) HeadElem() string {
if len(pb.elements) == 0 {
return ""
}
return pb.elements[0]
}
// LastElem returns the last element in the Builder.
func (pb Builder) LastElem() string {
if len(pb.elements) == 0 {
return ""
}
return pb.elements[len(pb.elements)-1]
}
// UpdateParent updates leading elements matching prev to be cur and returns
// true if it was updated. If prev is not a prefix of this Builder changes
// nothing and returns false. If either prev or cur is nil does nothing and
// returns false.
func (pb *Builder) UpdateParent(prev, cur *Builder) bool {
if prev == cur || prev == nil || cur == nil || len(prev.Elements()) > len(pb.Elements()) {
return false
}
parent := true
for i, e := range prev.Elements() {
if pb.elements[i] != e {
parent = false
break
}
}
if !parent {
return false
}
pb.elements = append(cur.Elements(), pb.elements[len(prev.Elements()):]...)
return true
}
// ShortRef produces a truncated hash of the builder that
// acts as a unique identifier.
func (pb Builder) ShortRef() string {
if len(pb.elements) == 0 {
return ""
}
data := bytes.Buffer{}
for _, element := range pb.elements {
data.WriteString(element)
}
sum := sha256.Sum256(data.Bytes())
// Some conversions to get the right number of characters in the output. This
// outputs hex, so we need to take the target number of characters and do the
// equivalent of (shortRefCharacters * 4) / 8. This is
// <number of bits represented> / <bits per byte> which gets us how many bytes
// to give to our format command.
numBytes := shortRefCharacters / 2
return fmt.Sprintf("%x", sum[:numBytes])
}
// Elements returns all the elements in the path. This is a temporary function
// and will likely be updated to handle encoded elements instead of clear-text
// elements in the future.
func (pb Builder) Elements() Elements {
return append(Elements{}, pb.elements...)
}
// withPrefix creates a Builder prefixed with the parameter values, and
// concatenated with the current builder elements.
func (pb Builder) withPrefix(elements ...string) *Builder {
res := Builder{}.Append(elements...)
res.elements = append(res.elements, pb.elements...)
return res
}
// verifyPrefix ensures that the tenant and resourceOwner are valid
// values, and that the builder has some directory structure.
func (pb Builder) verifyPrefix(tenant, resourceOwner string) error {
if err := verifyInputValues(tenant, resourceOwner); err != nil {
return err
}
if len(pb.elements) == 0 {
return clues.New("missing path beyond prefix")
}
return nil
}
// ---------------------------------------------------------------------------
// Data Layer Path Transformers
// ---------------------------------------------------------------------------
func (pb Builder) ToStreamStorePath(
tenant, purpose string,
service ServiceType,
isItem bool,
) (Path, error) {
if err := verifyInputValues(tenant, purpose); err != nil {
return nil, err
}
if isItem && len(pb.elements) == 0 {
return nil, clues.New("missing path beyond prefix")
}
metadataService := UnknownService
switch service {
case ExchangeService:
metadataService = ExchangeMetadataService
case OneDriveService:
metadataService = OneDriveMetadataService
case SharePointService:
metadataService = SharePointMetadataService
}
return &dataLayerResourcePath{
Builder: *pb.withPrefix(
tenant,
metadataService.String(),
purpose,
DetailsCategory.String()),
service: metadataService,
category: DetailsCategory,
hasItem: isItem,
}, nil
}
func (pb Builder) ToServiceCategoryMetadataPath(
tenant, user string,
service ServiceType,
category CategoryType,
isItem bool,
) (Path, error) {
if err := ValidateServiceAndCategory(service, category); err != nil {
return nil, err
}
if err := verifyInputValues(tenant, user); err != nil {
return nil, err
}
if isItem && len(pb.elements) == 0 {
return nil, clues.New("missing path beyond prefix")
}
metadataService := UnknownService
switch service {
case ExchangeService:
metadataService = ExchangeMetadataService
case OneDriveService:
metadataService = OneDriveMetadataService
case SharePointService:
metadataService = SharePointMetadataService
}
return &dataLayerResourcePath{
Builder: *pb.withPrefix(
tenant,
metadataService.String(),
user,
category.String(),
),
service: metadataService,
category: category,
hasItem: isItem,
}, nil
}
func (pb Builder) ToDataLayerPath(
tenant, user string,
service ServiceType,
category CategoryType,
isItem bool,
) (Path, error) {
if err := ValidateServiceAndCategory(service, category); err != nil {
return nil, err
}
if err := pb.verifyPrefix(tenant, user); err != nil {
return nil, err
}
return &dataLayerResourcePath{
Builder: *pb.withPrefix(
tenant,
service.String(),
user,
category.String()),
service: service,
category: category,
hasItem: isItem,
}, nil
}
func (pb Builder) ToDataLayerExchangePathForCategory(
tenant, user string,
category CategoryType,
isItem bool,
) (Path, error) {
return pb.ToDataLayerPath(tenant, user, ExchangeService, category, isItem)
}
func (pb Builder) ToDataLayerOneDrivePath(
tenant, user string,
isItem bool,
) (Path, error) {
return pb.ToDataLayerPath(tenant, user, OneDriveService, FilesCategory, isItem)
}
func (pb Builder) ToDataLayerSharePointPath(
tenant, site string,
category CategoryType,
isItem bool,
) (Path, error) {
return pb.ToDataLayerPath(tenant, site, SharePointService, category, isItem)
}
// ---------------------------------------------------------------------------
// Stringers and PII Concealer Compliance
// ---------------------------------------------------------------------------
// Conceal produces a concealed representation of the builder, suitable for
// logging, storing in errors, and other output.
func (pb Builder) Conceal() string {
return pb.elements.Conceal()
}
// Format produces a concealed representation of the builder, even when
// used within a PrintF, suitable for logging, storing in errors,
// and other output.
func (pb Builder) Format(fs fmt.State, _ rune) {
fmt.Fprint(fs, pb.Conceal())
}
// String returns a string that contains all path elements joined together.
// Elements of the path that need escaping are escaped.
// The result is not concealed, and is not suitable for logging or structured
// errors.
func (pb Builder) String() string {
return pb.elements.String()
}
// PlainString returns an unescaped, unmodified string of the builder.
// The result is not concealed, and is not suitable for logging or structured
// errors.
func (pb Builder) PlainString() string {
return pb.elements.PlainString()
}

View File

@ -0,0 +1,369 @@
package path
import (
"fmt"
"strings"
"testing"
"github.com/alcionai/clues"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/alcionai/corso/src/internal/tester"
)
type BuilderUnitSuite struct {
tester.Suite
}
func TestBuilderUnitSuite(t *testing.T) {
suite.Run(t, &BuilderUnitSuite{Suite: tester.NewUnitSuite(t)})
}
// set the clues hashing to mask for the span of this suite
func (suite *BuilderUnitSuite) SetupSuite() {
clues.SetHasher(clues.HashCfg{HashAlg: clues.Flatmask})
}
// revert clues hashing to plaintext for all other tests
func (suite *BuilderUnitSuite) TeardownSuite() {
clues.SetHasher(clues.NoHash())
}
func (suite *BuilderUnitSuite) TestAppend() {
table := append(append([]testData{}, genericCases...), basicUnescapedInputs...)
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
p := Builder{}.Append(test.input...)
assert.Equal(t, test.expectedString, p.String())
})
}
}
func (suite *BuilderUnitSuite) TestAppendItem() {
t := suite.T()
p, err := Build("t", "ro", ExchangeService, EmailCategory, false, "foo", "bar")
require.NoError(t, err, clues.ToCore(err))
pb := p.ToBuilder()
assert.Equal(t, pb.String(), p.String())
pb = pb.Append("qux")
p, err = p.AppendItem("qux")
require.NoError(t, err, clues.ToCore(err))
assert.Equal(t, pb.String(), p.String())
_, err = p.AppendItem("fnords")
require.Error(t, err, clues.ToCore(err))
}
func (suite *BuilderUnitSuite) TestUnescapeAndAppend() {
table := append(append([]testData{}, genericCases...), basicEscapedInputs...)
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
p, err := Builder{}.UnescapeAndAppend(test.input...)
require.NoError(t, err, clues.ToCore(err))
assert.Equal(t, test.expectedString, p.String())
})
}
}
func (suite *BuilderUnitSuite) TestEscapedFailure() {
target := "i_s"
for c := range charactersToEscape {
suite.Run(fmt.Sprintf("Unescaped-%c", c), func() {
tmp := strings.ReplaceAll(target, "_", string(c))
_, err := Builder{}.UnescapeAndAppend("this", tmp, "path")
assert.Errorf(suite.T(), err, "path with unescaped %s did not error", string(c))
})
}
}
func (suite *BuilderUnitSuite) TestBadEscapeSequenceErrors() {
target := `i\_s/a`
notEscapes := []rune{'a', 'b', '#', '%'}
for _, c := range notEscapes {
suite.Run(fmt.Sprintf("Escaped-%c", c), func() {
tmp := strings.ReplaceAll(target, "_", string(c))
_, err := Builder{}.UnescapeAndAppend("this", tmp, "path")
assert.Errorf(
suite.T(),
err,
"path with bad escape sequence %c%c did not error",
escapeCharacter,
c)
})
}
}
func (suite *BuilderUnitSuite) TestTrailingEscapeChar() {
base := []string{"this", "is", "a", "path"}
for i := 0; i < len(base); i++ {
suite.Run(fmt.Sprintf("Element%v", i), func() {
path := make([]string, len(base))
copy(path, base)
path[i] = path[i] + string(escapeCharacter)
_, err := Builder{}.UnescapeAndAppend(path...)
assert.Error(
suite.T(),
err,
"path with trailing escape character did not error")
})
}
}
func (suite *BuilderUnitSuite) TestElements() {
table := []struct {
name string
input []string
output []string
pathFunc func(elements []string) (*Builder, error)
}{
{
name: "SimpleEscapedPath",
input: []string{"this", "is", "a", "path"},
output: []string{"this", "is", "a", "path"},
pathFunc: func(elements []string) (*Builder, error) {
return Builder{}.UnescapeAndAppend(elements...)
},
},
{
name: "SimpleUnescapedPath",
input: []string{"this", "is", "a", "path"},
output: []string{"this", "is", "a", "path"},
pathFunc: func(elements []string) (*Builder, error) {
return Builder{}.Append(elements...), nil
},
},
{
name: "EscapedPath",
input: []string{"this", `is\/`, "a", "path"},
output: []string{"this", "is/", "a", "path"},
pathFunc: func(elements []string) (*Builder, error) {
return Builder{}.UnescapeAndAppend(elements...)
},
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
p, err := test.pathFunc(test.input)
require.NoError(t, err, clues.ToCore(err))
assert.Equal(t, Elements(test.output), p.Elements())
})
}
}
func (suite *BuilderUnitSuite) TestPopFront() {
table := []struct {
name string
base *Builder
expectedString string
}{
{
name: "Empty",
base: &Builder{},
expectedString: "",
},
{
name: "OneElement",
base: Builder{}.Append("something"),
expectedString: "",
},
{
name: "TwoElements",
base: Builder{}.Append("something", "else"),
expectedString: "else",
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
assert.Equal(t, test.expectedString, test.base.PopFront().String())
})
}
}
func (suite *BuilderUnitSuite) TestShortRef() {
table := []struct {
name string
inputElements []string
expectedLen int
}{
{
name: "PopulatedPath",
inputElements: []string{"this", "is", "a", "path"},
expectedLen: shortRefCharacters,
},
{
name: "EmptyPath",
inputElements: nil,
expectedLen: 0,
},
}
for _, test := range table {
suite.Run(test.name, func() {
pb := Builder{}.Append(test.inputElements...)
ref := pb.ShortRef()
assert.Len(suite.T(), ref, test.expectedLen)
})
}
}
func (suite *BuilderUnitSuite) TestShortRefIsStable() {
t := suite.T()
pb := Builder{}.Append("this", "is", "a", "path")
prevRef := pb.ShortRef()
assert.Len(t, prevRef, shortRefCharacters)
for i := 0; i < 5; i++ {
ref := pb.ShortRef()
assert.Len(t, ref, shortRefCharacters)
assert.Equal(t, prevRef, ref, "ShortRef changed between calls")
prevRef = ref
}
}
func (suite *BuilderUnitSuite) TestShortRefIsUnique() {
pb1 := Builder{}.Append("this", "is", "a", "path")
pb2 := pb1.Append("also")
require.NotEqual(suite.T(), pb1, pb2)
assert.NotEqual(suite.T(), pb1.ShortRef(), pb2.ShortRef())
}
// TestShortRefUniqueWithEscaping tests that two paths that output the same
// unescaped string but different escaped strings have different shortrefs. This
// situation can occur when one path has embedded path separators while the
// other does not but contains the same characters.
func (suite *BuilderUnitSuite) TestShortRefUniqueWithEscaping() {
pb1 := Builder{}.Append(`this`, `is`, `a`, `path`)
pb2 := Builder{}.Append(`this`, `is/a`, `path`)
require.NotEqual(suite.T(), pb1, pb2)
assert.NotEqual(suite.T(), pb1.ShortRef(), pb2.ShortRef())
}
func (suite *BuilderUnitSuite) TestFolder() {
table := []struct {
name string
p func(t *testing.T) Path
escape bool
expectFolder string
expectSplit []string
}{
{
name: "clean path",
p: func(t *testing.T) Path {
p, err := Builder{}.
Append("a", "b", "c").
ToDataLayerExchangePathForCategory("t", "u", EmailCategory, false)
require.NoError(t, err, clues.ToCore(err))
return p
},
expectFolder: "a/b/c",
expectSplit: []string{"a", "b", "c"},
},
{
name: "clean path escaped",
p: func(t *testing.T) Path {
p, err := Builder{}.
Append("a", "b", "c").
ToDataLayerExchangePathForCategory("t", "u", EmailCategory, false)
require.NoError(t, err, clues.ToCore(err))
return p
},
escape: true,
expectFolder: "a/b/c",
expectSplit: []string{"a", "b", "c"},
},
{
name: "escapable path",
p: func(t *testing.T) Path {
p, err := Builder{}.
Append("a/", "b", "c").
ToDataLayerExchangePathForCategory("t", "u", EmailCategory, false)
require.NoError(t, err, clues.ToCore(err))
return p
},
expectFolder: "a//b/c",
expectSplit: []string{"a", "b", "c"},
},
{
name: "escapable path escaped",
p: func(t *testing.T) Path {
p, err := Builder{}.
Append("a/", "b", "c").
ToDataLayerExchangePathForCategory("t", "u", EmailCategory, false)
require.NoError(t, err, clues.ToCore(err))
return p
},
escape: true,
expectFolder: "a\\//b/c",
expectSplit: []string{"a\\/", "b", "c"},
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
p := test.p(t)
result := p.Folder(test.escape)
assert.Equal(t, test.expectFolder, result)
assert.Equal(t, test.expectSplit, Split(result))
})
}
}
func (suite *BuilderUnitSuite) TestPIIHandling() {
p, err := Build("t", "ro", ExchangeService, EventsCategory, true, "dir", "item")
require.NoError(suite.T(), err)
table := []struct {
name string
p Path
expect string
expectPlain string
}{
{
name: "standard path",
p: p,
expect: "***/exchange/***/events/***/***",
expectPlain: "t/exchange/ro/events/dir/item",
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
assert.Equal(t, test.expect, test.p.Conceal(), "conceal")
assert.Equal(t, test.expectPlain, test.p.String(), "string")
assert.Equal(t, test.expect, fmt.Sprintf("%s", test.p), "fmt %%s")
assert.Equal(t, test.expect, fmt.Sprintf("%+v", test.p), "fmt %%+v")
assert.Equal(t, test.expectPlain, test.p.PlainString(), "plain")
})
}
}

View File

@ -0,0 +1,108 @@
package path
import (
"fmt"
"strings"
"github.com/alcionai/clues"
)
var ErrorUnknownCategory = clues.New("unknown category string")
// CategoryType denotes what category of data the path corresponds to. The order
// of the enums below can be changed, but the string representation of each enum
// must remain the same or migration code needs to be added to handle changes to
// the string format.
type CategoryType int
//go:generate stringer -type=CategoryType -linecomment
const (
UnknownCategory CategoryType = iota
EmailCategory // email
ContactsCategory // contacts
EventsCategory // events
FilesCategory // files
ListsCategory // lists
LibrariesCategory // libraries
PagesCategory // pages
DetailsCategory // details
)
func ToCategoryType(category string) CategoryType {
cat := strings.ToLower(category)
switch cat {
case strings.ToLower(EmailCategory.String()):
return EmailCategory
case strings.ToLower(ContactsCategory.String()):
return ContactsCategory
case strings.ToLower(EventsCategory.String()):
return EventsCategory
case strings.ToLower(FilesCategory.String()):
return FilesCategory
case strings.ToLower(LibrariesCategory.String()):
return LibrariesCategory
case strings.ToLower(ListsCategory.String()):
return ListsCategory
case strings.ToLower(PagesCategory.String()):
return PagesCategory
case strings.ToLower(DetailsCategory.String()):
return DetailsCategory
default:
return UnknownCategory
}
}
// ---------------------------------------------------------------------------
// Service-Category pairings
// ---------------------------------------------------------------------------
// serviceCategories is a mapping of all valid service/category pairs for
// non-metadata paths.
var serviceCategories = map[ServiceType]map[CategoryType]struct{}{
ExchangeService: {
EmailCategory: {},
ContactsCategory: {},
EventsCategory: {},
},
OneDriveService: {
FilesCategory: {},
},
SharePointService: {
LibrariesCategory: {},
ListsCategory: {},
PagesCategory: {},
},
}
func validateServiceAndCategoryStrings(s, c string) (ServiceType, CategoryType, error) {
service := toServiceType(s)
if service == UnknownService {
return UnknownService, UnknownCategory, clues.Stack(ErrorUnknownService).With("service", fmt.Sprintf("%q", s))
}
category := ToCategoryType(c)
if category == UnknownCategory {
return UnknownService, UnknownCategory, clues.Stack(ErrorUnknownService).With("category", fmt.Sprintf("%q", c))
}
if err := ValidateServiceAndCategory(service, category); err != nil {
return UnknownService, UnknownCategory, err
}
return service, category, nil
}
func ValidateServiceAndCategory(service ServiceType, category CategoryType) error {
cats, ok := serviceCategories[service]
if !ok {
return clues.New("unsupported service").With("service", fmt.Sprintf("%q", service))
}
if _, ok := cats[category]; !ok {
return clues.New("unknown service/category combination").
With("service", fmt.Sprintf("%q", service), "category", fmt.Sprintf("%q", category))
}
return nil
}

View File

@ -2,6 +2,7 @@ package path
import "github.com/alcionai/clues"
// TODO: Move this into m365/collection/drive
// drivePath is used to represent path components
// of an item within the drive i.e.
// Given `drives/b!X_8Z2zuXpkKkXZsr7gThk9oJpuj0yXVGnK5_VjRRPK-q725SX_8ZQJgFDK8PlFxA/root:/Folder1/Folder2/file`

View File

@ -51,8 +51,6 @@
package path
import (
"bytes"
"crypto/sha256"
"fmt"
"strings"
@ -122,12 +120,6 @@ type Path interface {
fmt.Stringer
}
// interface compliance required for handling PII
var (
_ clues.Concealer = &Builder{}
_ fmt.Stringer = &Builder{}
)
// RestorePaths denotes the location to find an item in kopia and the path of
// the collection to place the item in for restore.
type RestorePaths struct {
@ -135,185 +127,26 @@ type RestorePaths struct {
RestorePath Path
}
// Builder is a simple path representation that only tracks path elements. It
// can join, escape, and unescape elements. Higher-level packages are expected
// to wrap this struct to build resource-specific contexts (e.x. an
// ExchangeMailPath).
// Resource-specific paths allow access to more information like segments in the
// path. Builders that are turned into resource paths later on do not need to
// manually add prefixes for items that normally appear in the data layer (ex.
// tenant ID, service, user ID, etc).
type Builder struct {
// Unescaped version of elements.
elements Elements
// ---------------------------------------------------------------------------
// Exported Helpers
// ---------------------------------------------------------------------------
func Build(
tenant, resourceOwner string,
service ServiceType,
category CategoryType,
hasItem bool,
elements ...string,
) (Path, error) {
b := Builder{}.Append(elements...)
return b.ToDataLayerPath(
tenant, resourceOwner,
service, category,
hasItem)
}
// Append creates a copy of this Builder and adds the given elements them to the
// end of the new Builder. Elements are added in the order they are passed.
func (pb Builder) Append(elements ...string) *Builder {
res := &Builder{elements: make([]string, len(pb.elements))}
copy(res.elements, pb.elements)
// Unescaped elements can't fail validation.
//nolint:errcheck
res.appendElements(false, elements)
return res
}
func (pb *Builder) appendElements(escaped bool, elements []string) error {
for _, e := range elements {
if len(e) == 0 {
continue
}
tmp := e
if escaped {
tmp = TrimTrailingSlash(tmp)
// If tmp was just the path separator then it will be empty now.
if len(tmp) == 0 {
continue
}
if err := validateEscapedElement(tmp); err != nil {
return err
}
tmp = unescape(tmp)
}
pb.elements = append(pb.elements, tmp)
}
return nil
}
// UnescapeAndAppend creates a copy of this Builder and adds one or more already
// escaped path elements to the end of the new Builder. Elements are added in
// the order they are passed.
func (pb Builder) UnescapeAndAppend(elements ...string) (*Builder, error) {
res := &Builder{elements: make([]string, 0, len(pb.elements))}
copy(res.elements, pb.elements)
if err := res.appendElements(true, elements); err != nil {
return nil, err
}
return res, nil
}
// SplitUnescapeAppend takes in an escaped string representing a directory
// path, splits the string, and appends it to the current builder.
func (pb Builder) SplitUnescapeAppend(s string) (*Builder, error) {
elems := Split(TrimTrailingSlash(s))
return pb.UnescapeAndAppend(elems...)
}
func (pb Builder) PopFront() *Builder {
if len(pb.elements) <= 1 {
return &Builder{}
}
elements := make([]string, len(pb.elements)-1)
copy(elements, pb.elements[1:])
return &Builder{
elements: elements,
}
}
// Dir removes the last element from the builder.
func (pb Builder) Dir() *Builder {
if len(pb.elements) <= 1 {
return &Builder{}
}
return &Builder{
// Safe to use the same elements because Builders are immutable.
elements: pb.elements[:len(pb.elements)-1],
}
}
// HeadElem returns the first element in the Builder.
func (pb Builder) HeadElem() string {
if len(pb.elements) == 0 {
return ""
}
return pb.elements[0]
}
// LastElem returns the last element in the Builder.
func (pb Builder) LastElem() string {
if len(pb.elements) == 0 {
return ""
}
return pb.elements[len(pb.elements)-1]
}
// UpdateParent updates leading elements matching prev to be cur and returns
// true if it was updated. If prev is not a prefix of this Builder changes
// nothing and returns false. If either prev or cur is nil does nothing and
// returns false.
func (pb *Builder) UpdateParent(prev, cur *Builder) bool {
if prev == cur || prev == nil || cur == nil || len(prev.Elements()) > len(pb.Elements()) {
return false
}
parent := true
for i, e := range prev.Elements() {
if pb.elements[i] != e {
parent = false
break
}
}
if !parent {
return false
}
pb.elements = append(cur.Elements(), pb.elements[len(prev.Elements()):]...)
return true
}
// ShortRef produces a truncated hash of the builder that
// acts as a unique identifier.
func (pb Builder) ShortRef() string {
if len(pb.elements) == 0 {
return ""
}
data := bytes.Buffer{}
for _, element := range pb.elements {
data.WriteString(element)
}
sum := sha256.Sum256(data.Bytes())
// Some conversions to get the right number of characters in the output. This
// outputs hex, so we need to take the target number of characters and do the
// equivalent of (shortRefCharacters * 4) / 8. This is
// <number of bits represented> / <bits per byte> which gets us how many bytes
// to give to our format command.
numBytes := shortRefCharacters / 2
return fmt.Sprintf("%x", sum[:numBytes])
}
// Elements returns all the elements in the path. This is a temporary function
// and will likely be updated to handle encoded elements instead of clear-text
// elements in the future.
func (pb Builder) Elements() Elements {
return append(Elements{}, pb.elements...)
}
func ServicePrefix(
func BuildPrefix(
tenant, resourceOwner string,
s ServiceType,
c CategoryType,
@ -336,197 +169,6 @@ func ServicePrefix(
}, nil
}
// withPrefix creates a Builder prefixed with the parameter values, and
// concatenated with the current builder elements.
func (pb Builder) withPrefix(elements ...string) *Builder {
res := Builder{}.Append(elements...)
res.elements = append(res.elements, pb.elements...)
return res
}
// ---------------------------------------------------------------------------
// Data Layer Path Transformers
// ---------------------------------------------------------------------------
func (pb Builder) ToStreamStorePath(
tenant, purpose string,
service ServiceType,
isItem bool,
) (Path, error) {
if err := verifyInputValues(tenant, purpose); err != nil {
return nil, err
}
if isItem && len(pb.elements) == 0 {
return nil, clues.New("missing path beyond prefix")
}
metadataService := UnknownService
switch service {
case ExchangeService:
metadataService = ExchangeMetadataService
case OneDriveService:
metadataService = OneDriveMetadataService
case SharePointService:
metadataService = SharePointMetadataService
}
return &dataLayerResourcePath{
Builder: *pb.withPrefix(
tenant,
metadataService.String(),
purpose,
DetailsCategory.String()),
service: metadataService,
category: DetailsCategory,
hasItem: isItem,
}, nil
}
func (pb Builder) ToServiceCategoryMetadataPath(
tenant, user string,
service ServiceType,
category CategoryType,
isItem bool,
) (Path, error) {
if err := ValidateServiceAndCategory(service, category); err != nil {
return nil, err
}
if err := verifyInputValues(tenant, user); err != nil {
return nil, err
}
if isItem && len(pb.elements) == 0 {
return nil, clues.New("missing path beyond prefix")
}
metadataService := UnknownService
switch service {
case ExchangeService:
metadataService = ExchangeMetadataService
case OneDriveService:
metadataService = OneDriveMetadataService
case SharePointService:
metadataService = SharePointMetadataService
}
return &dataLayerResourcePath{
Builder: *pb.withPrefix(
tenant,
metadataService.String(),
user,
category.String(),
),
service: metadataService,
category: category,
hasItem: isItem,
}, nil
}
func (pb Builder) ToDataLayerPath(
tenant, user string,
service ServiceType,
category CategoryType,
isItem bool,
) (Path, error) {
if err := ValidateServiceAndCategory(service, category); err != nil {
return nil, err
}
if err := pb.verifyPrefix(tenant, user); err != nil {
return nil, err
}
return &dataLayerResourcePath{
Builder: *pb.withPrefix(
tenant,
service.String(),
user,
category.String()),
service: service,
category: category,
hasItem: isItem,
}, nil
}
func (pb Builder) ToDataLayerExchangePathForCategory(
tenant, user string,
category CategoryType,
isItem bool,
) (Path, error) {
return pb.ToDataLayerPath(tenant, user, ExchangeService, category, isItem)
}
func (pb Builder) ToDataLayerOneDrivePath(
tenant, user string,
isItem bool,
) (Path, error) {
return pb.ToDataLayerPath(tenant, user, OneDriveService, FilesCategory, isItem)
}
func (pb Builder) ToDataLayerSharePointPath(
tenant, site string,
category CategoryType,
isItem bool,
) (Path, error) {
return pb.ToDataLayerPath(tenant, site, SharePointService, category, isItem)
}
// ---------------------------------------------------------------------------
// Stringers and PII Concealer Compliance
// ---------------------------------------------------------------------------
// Conceal produces a concealed representation of the builder, suitable for
// logging, storing in errors, and other output.
func (pb Builder) Conceal() string {
return pb.elements.Conceal()
}
// Format produces a concealed representation of the builder, even when
// used within a PrintF, suitable for logging, storing in errors,
// and other output.
func (pb Builder) Format(fs fmt.State, _ rune) {
fmt.Fprint(fs, pb.Conceal())
}
// String returns a string that contains all path elements joined together.
// Elements of the path that need escaping are escaped.
// The result is not concealed, and is not suitable for logging or structured
// errors.
func (pb Builder) String() string {
return pb.elements.String()
}
// PlainString returns an unescaped, unmodified string of the builder.
// The result is not concealed, and is not suitable for logging or structured
// errors.
func (pb Builder) PlainString() string {
return pb.elements.PlainString()
}
// ---------------------------------------------------------------------------
// Exported Helpers
// ---------------------------------------------------------------------------
func Build(
tenant, resourceOwner string,
service ServiceType,
category CategoryType,
hasItem bool,
elements ...string,
) (Path, error) {
b := Builder{}.Append(elements...)
return b.ToDataLayerPath(
tenant, resourceOwner,
service, category,
hasItem)
}
// FromDataLayerPath parses the escaped path p, validates the elements in p
// match a resource-specific path format, and returns a Path struct for that
// resource-specific type. If p does not match any resource-specific paths or
@ -762,17 +404,3 @@ func join(elements []string) string {
// '\' according to the escaping rules.
return strings.Join(elements, string(PathSeparator))
}
// verifyPrefix ensures that the tenant and resourceOwner are valid
// values, and that the builder has some directory structure.
func (pb Builder) verifyPrefix(tenant, resourceOwner string) error {
if err := verifyInputValues(tenant, resourceOwner); err != nil {
return err
}
if len(pb.elements) == 0 {
return clues.New("missing path beyond prefix")
}
return nil
}

View File

@ -2,7 +2,6 @@ package path
import (
"fmt"
"strings"
"testing"
"github.com/alcionai/clues"
@ -233,239 +232,7 @@ func (suite *PathUnitSuite) TeardownSuite() {
clues.SetHasher(clues.NoHash())
}
func (suite *PathUnitSuite) TestAppend() {
table := append(append([]testData{}, genericCases...), basicUnescapedInputs...)
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
p := Builder{}.Append(test.input...)
assert.Equal(t, test.expectedString, p.String())
})
}
}
func (suite *PathUnitSuite) TestAppendItem() {
t := suite.T()
p, err := Build("t", "ro", ExchangeService, EmailCategory, false, "foo", "bar")
require.NoError(t, err, clues.ToCore(err))
pb := p.ToBuilder()
assert.Equal(t, pb.String(), p.String())
pb = pb.Append("qux")
p, err = p.AppendItem("qux")
require.NoError(t, err, clues.ToCore(err))
assert.Equal(t, pb.String(), p.String())
_, err = p.AppendItem("fnords")
require.Error(t, err, clues.ToCore(err))
}
func (suite *PathUnitSuite) TestUnescapeAndAppend() {
table := append(append([]testData{}, genericCases...), basicEscapedInputs...)
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
p, err := Builder{}.UnescapeAndAppend(test.input...)
require.NoError(t, err, clues.ToCore(err))
assert.Equal(t, test.expectedString, p.String())
})
}
}
func (suite *PathUnitSuite) TestEscapedFailure() {
target := "i_s"
for c := range charactersToEscape {
suite.Run(fmt.Sprintf("Unescaped-%c", c), func() {
tmp := strings.ReplaceAll(target, "_", string(c))
_, err := Builder{}.UnescapeAndAppend("this", tmp, "path")
assert.Errorf(suite.T(), err, "path with unescaped %s did not error", string(c))
})
}
}
func (suite *PathUnitSuite) TestBadEscapeSequenceErrors() {
target := `i\_s/a`
notEscapes := []rune{'a', 'b', '#', '%'}
for _, c := range notEscapes {
suite.Run(fmt.Sprintf("Escaped-%c", c), func() {
tmp := strings.ReplaceAll(target, "_", string(c))
_, err := Builder{}.UnescapeAndAppend("this", tmp, "path")
assert.Errorf(
suite.T(),
err,
"path with bad escape sequence %c%c did not error",
escapeCharacter,
c)
})
}
}
func (suite *PathUnitSuite) TestTrailingEscapeChar() {
base := []string{"this", "is", "a", "path"}
for i := 0; i < len(base); i++ {
suite.Run(fmt.Sprintf("Element%v", i), func() {
path := make([]string, len(base))
copy(path, base)
path[i] = path[i] + string(escapeCharacter)
_, err := Builder{}.UnescapeAndAppend(path...)
assert.Error(
suite.T(),
err,
"path with trailing escape character did not error")
})
}
}
func (suite *PathUnitSuite) TestElements() {
table := []struct {
name string
input []string
output []string
pathFunc func(elements []string) (*Builder, error)
}{
{
name: "SimpleEscapedPath",
input: []string{"this", "is", "a", "path"},
output: []string{"this", "is", "a", "path"},
pathFunc: func(elements []string) (*Builder, error) {
return Builder{}.UnescapeAndAppend(elements...)
},
},
{
name: "SimpleUnescapedPath",
input: []string{"this", "is", "a", "path"},
output: []string{"this", "is", "a", "path"},
pathFunc: func(elements []string) (*Builder, error) {
return Builder{}.Append(elements...), nil
},
},
{
name: "EscapedPath",
input: []string{"this", `is\/`, "a", "path"},
output: []string{"this", "is/", "a", "path"},
pathFunc: func(elements []string) (*Builder, error) {
return Builder{}.UnescapeAndAppend(elements...)
},
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
p, err := test.pathFunc(test.input)
require.NoError(t, err, clues.ToCore(err))
assert.Equal(t, Elements(test.output), p.Elements())
})
}
}
func (suite *PathUnitSuite) TestPopFront() {
table := []struct {
name string
base *Builder
expectedString string
}{
{
name: "Empty",
base: &Builder{},
expectedString: "",
},
{
name: "OneElement",
base: Builder{}.Append("something"),
expectedString: "",
},
{
name: "TwoElements",
base: Builder{}.Append("something", "else"),
expectedString: "else",
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
assert.Equal(t, test.expectedString, test.base.PopFront().String())
})
}
}
func (suite *PathUnitSuite) TestShortRef() {
table := []struct {
name string
inputElements []string
expectedLen int
}{
{
name: "PopulatedPath",
inputElements: []string{"this", "is", "a", "path"},
expectedLen: shortRefCharacters,
},
{
name: "EmptyPath",
inputElements: nil,
expectedLen: 0,
},
}
for _, test := range table {
suite.Run(test.name, func() {
pb := Builder{}.Append(test.inputElements...)
ref := pb.ShortRef()
assert.Len(suite.T(), ref, test.expectedLen)
})
}
}
func (suite *PathUnitSuite) TestShortRefIsStable() {
t := suite.T()
pb := Builder{}.Append("this", "is", "a", "path")
prevRef := pb.ShortRef()
assert.Len(t, prevRef, shortRefCharacters)
for i := 0; i < 5; i++ {
ref := pb.ShortRef()
assert.Len(t, ref, shortRefCharacters)
assert.Equal(t, prevRef, ref, "ShortRef changed between calls")
prevRef = ref
}
}
func (suite *PathUnitSuite) TestShortRefIsUnique() {
pb1 := Builder{}.Append("this", "is", "a", "path")
pb2 := pb1.Append("also")
require.NotEqual(suite.T(), pb1, pb2)
assert.NotEqual(suite.T(), pb1.ShortRef(), pb2.ShortRef())
}
// TestShortRefUniqueWithEscaping tests that two paths that output the same
// unescaped string but different escaped strings have different shortrefs. This
// situation can occur when one path has embedded path separators while the
// other does not but contains the same characters.
func (suite *PathUnitSuite) TestShortRefUniqueWithEscaping() {
pb1 := Builder{}.Append(`this`, `is`, `a`, `path`)
pb2 := Builder{}.Append(`this`, `is/a`, `path`)
require.NotEqual(suite.T(), pb1, pb2)
assert.NotEqual(suite.T(), pb1.ShortRef(), pb2.ShortRef())
}
func (suite *PathUnitSuite) TestFromStringErrors() {
func (suite *PathUnitSuite) TestFromDataLayerPathErrors() {
table := []struct {
name string
escapedPath string
@ -521,82 +288,7 @@ func (suite *PathUnitSuite) TestFromStringErrors() {
}
}
func (suite *PathUnitSuite) TestFolder() {
table := []struct {
name string
p func(t *testing.T) Path
escape bool
expectFolder string
expectSplit []string
}{
{
name: "clean path",
p: func(t *testing.T) Path {
p, err := Builder{}.
Append("a", "b", "c").
ToDataLayerExchangePathForCategory("t", "u", EmailCategory, false)
require.NoError(t, err, clues.ToCore(err))
return p
},
expectFolder: "a/b/c",
expectSplit: []string{"a", "b", "c"},
},
{
name: "clean path escaped",
p: func(t *testing.T) Path {
p, err := Builder{}.
Append("a", "b", "c").
ToDataLayerExchangePathForCategory("t", "u", EmailCategory, false)
require.NoError(t, err, clues.ToCore(err))
return p
},
escape: true,
expectFolder: "a/b/c",
expectSplit: []string{"a", "b", "c"},
},
{
name: "escapable path",
p: func(t *testing.T) Path {
p, err := Builder{}.
Append("a/", "b", "c").
ToDataLayerExchangePathForCategory("t", "u", EmailCategory, false)
require.NoError(t, err, clues.ToCore(err))
return p
},
expectFolder: "a//b/c",
expectSplit: []string{"a", "b", "c"},
},
{
name: "escapable path escaped",
p: func(t *testing.T) Path {
p, err := Builder{}.
Append("a/", "b", "c").
ToDataLayerExchangePathForCategory("t", "u", EmailCategory, false)
require.NoError(t, err, clues.ToCore(err))
return p
},
escape: true,
expectFolder: "a\\//b/c",
expectSplit: []string{"a\\/", "b", "c"},
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
p := test.p(t)
result := p.Folder(test.escape)
assert.Equal(t, test.expectFolder, result)
assert.Equal(t, test.expectSplit, Split(result))
})
}
}
func (suite *PathUnitSuite) TestFromString() {
func (suite *PathUnitSuite) TestFromDataLayerPath() {
const (
testTenant = "tenant"
testUser = "user"
@ -740,37 +432,7 @@ func (suite *PathUnitSuite) TestFromString() {
}
}
func (suite *PathUnitSuite) TestPath_piiHandling() {
p, err := Build("t", "ro", ExchangeService, EventsCategory, true, "dir", "item")
require.NoError(suite.T(), err)
table := []struct {
name string
p Path
expect string
expectPlain string
}{
{
name: "standard path",
p: p,
expect: "***/exchange/***/events/***/***",
expectPlain: "t/exchange/ro/events/dir/item",
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
assert.Equal(t, test.expect, test.p.Conceal(), "conceal")
assert.Equal(t, test.expectPlain, test.p.String(), "string")
assert.Equal(t, test.expect, fmt.Sprintf("%s", test.p), "fmt %%s")
assert.Equal(t, test.expect, fmt.Sprintf("%+v", test.p), "fmt %%+v")
assert.Equal(t, test.expectPlain, test.p.PlainString(), "plain")
})
}
}
func (suite *PathUnitSuite) TestToServicePrefix() {
func (suite *PathUnitSuite) TestBuildPrefix() {
table := []struct {
name string
service ServiceType
@ -818,7 +480,7 @@ func (suite *PathUnitSuite) TestToServicePrefix() {
suite.Run(test.name, func() {
t := suite.T()
r, err := ServicePrefix(test.tenant, test.owner, test.service, test.category)
r, err := BuildPrefix(test.tenant, test.owner, test.service, test.category)
test.expectErr(t, err, clues.ToCore(err))
if r == nil {

View File

@ -1,154 +1,9 @@
package path
import (
"fmt"
"strings"
"github.com/alcionai/clues"
)
var ErrorUnknownService = clues.New("unknown service string")
// ServiceType denotes what service the path corresponds to. Metadata services
// are also included though they are only used for paths that house metadata for
// Corso backups.
//
// Metadata services are not considered valid service types for resource paths
// though they can be used for metadata paths.
//
// The order of the enums below can be changed, but the string representation of
// each enum must remain the same or migration code needs to be added to handle
// changes to the string format.
type ServiceType int
//go:generate stringer -type=ServiceType -linecomment
const (
UnknownService ServiceType = iota
ExchangeService // exchange
OneDriveService // onedrive
SharePointService // sharepoint
ExchangeMetadataService // exchangeMetadata
OneDriveMetadataService // onedriveMetadata
SharePointMetadataService // sharepointMetadata
)
func toServiceType(service string) ServiceType {
s := strings.ToLower(service)
switch s {
case strings.ToLower(ExchangeService.String()):
return ExchangeService
case strings.ToLower(OneDriveService.String()):
return OneDriveService
case strings.ToLower(SharePointService.String()):
return SharePointService
case strings.ToLower(ExchangeMetadataService.String()):
return ExchangeMetadataService
case strings.ToLower(OneDriveMetadataService.String()):
return OneDriveMetadataService
case strings.ToLower(SharePointMetadataService.String()):
return SharePointMetadataService
default:
return UnknownService
}
}
var ErrorUnknownCategory = clues.New("unknown category string")
// CategoryType denotes what category of data the path corresponds to. The order
// of the enums below can be changed, but the string representation of each enum
// must remain the same or migration code needs to be added to handle changes to
// the string format.
type CategoryType int
//go:generate stringer -type=CategoryType -linecomment
const (
UnknownCategory CategoryType = iota
EmailCategory // email
ContactsCategory // contacts
EventsCategory // events
FilesCategory // files
ListsCategory // lists
LibrariesCategory // libraries
PagesCategory // pages
DetailsCategory // details
)
func ToCategoryType(category string) CategoryType {
cat := strings.ToLower(category)
switch cat {
case strings.ToLower(EmailCategory.String()):
return EmailCategory
case strings.ToLower(ContactsCategory.String()):
return ContactsCategory
case strings.ToLower(EventsCategory.String()):
return EventsCategory
case strings.ToLower(FilesCategory.String()):
return FilesCategory
case strings.ToLower(LibrariesCategory.String()):
return LibrariesCategory
case strings.ToLower(ListsCategory.String()):
return ListsCategory
case strings.ToLower(PagesCategory.String()):
return PagesCategory
case strings.ToLower(DetailsCategory.String()):
return DetailsCategory
default:
return UnknownCategory
}
}
// serviceCategories is a mapping of all valid service/category pairs for
// non-metadata paths.
var serviceCategories = map[ServiceType]map[CategoryType]struct{}{
ExchangeService: {
EmailCategory: {},
ContactsCategory: {},
EventsCategory: {},
},
OneDriveService: {
FilesCategory: {},
},
SharePointService: {
LibrariesCategory: {},
ListsCategory: {},
PagesCategory: {},
},
}
func validateServiceAndCategoryStrings(s, c string) (ServiceType, CategoryType, error) {
service := toServiceType(s)
if service == UnknownService {
return UnknownService, UnknownCategory, clues.Stack(ErrorUnknownService).With("service", fmt.Sprintf("%q", s))
}
category := ToCategoryType(c)
if category == UnknownCategory {
return UnknownService, UnknownCategory, clues.Stack(ErrorUnknownService).With("category", fmt.Sprintf("%q", c))
}
if err := ValidateServiceAndCategory(service, category); err != nil {
return UnknownService, UnknownCategory, err
}
return service, category, nil
}
func ValidateServiceAndCategory(service ServiceType, category CategoryType) error {
cats, ok := serviceCategories[service]
if !ok {
return clues.New("unsupported service").With("service", fmt.Sprintf("%q", service))
}
if _, ok := cats[category]; !ok {
return clues.New("unknown service/category combination").
With("service", fmt.Sprintf("%q", service), "category", fmt.Sprintf("%q", category))
}
return nil
}
// dataLayerResourcePath allows callers to extract information from a
// resource-specific path. This struct is unexported so that callers are
// forced to use the pre-defined constructors, making it impossible to create a

View File

@ -0,0 +1,53 @@
package path
import (
"strings"
"github.com/alcionai/clues"
)
var ErrorUnknownService = clues.New("unknown service string")
// ServiceType denotes what service the path corresponds to. Metadata services
// are also included though they are only used for paths that house metadata for
// Corso backups.
//
// Metadata services are not considered valid service types for resource paths
// though they can be used for metadata paths.
//
// The order of the enums below can be changed, but the string representation of
// each enum must remain the same or migration code needs to be added to handle
// changes to the string format.
type ServiceType int
//go:generate stringer -type=ServiceType -linecomment
const (
UnknownService ServiceType = iota
ExchangeService // exchange
OneDriveService // onedrive
SharePointService // sharepoint
ExchangeMetadataService // exchangeMetadata
OneDriveMetadataService // onedriveMetadata
SharePointMetadataService // sharepointMetadata
)
func toServiceType(service string) ServiceType {
s := strings.ToLower(service)
switch s {
case strings.ToLower(ExchangeService.String()):
return ExchangeService
case strings.ToLower(OneDriveService.String()):
return OneDriveService
case strings.ToLower(SharePointService.String()):
return SharePointService
case strings.ToLower(ExchangeMetadataService.String()):
return ExchangeMetadataService
case strings.ToLower(OneDriveMetadataService.String()):
return OneDriveMetadataService
case strings.ToLower(SharePointMetadataService.String()):
return SharePointMetadataService
default:
return UnknownService
}
}

View File

@ -38,7 +38,7 @@ func (br backupReason) Category() path.CategoryType {
}
func (br backupReason) SubtreePath() (path.Path, error) {
return path.ServicePrefix(
return path.BuildPrefix(
br.tenant,
br.resource,
br.service,