diff --git a/pkg/dnservice/store.go b/pkg/dnservice/store.go index 5565ca19019768bff03469b7f1d112202c1ec9bb..b810dec73dc8618d7ae1b7903af7346493b1f330 100644 --- a/pkg/dnservice/store.go +++ b/pkg/dnservice/store.go @@ -394,7 +394,10 @@ func (s *store) initFileService() error { return err } - s.fs = fileservice.NewFileServices(localFileServiceName, localFS, s3FS) + s.fs, err = fileservice.NewFileServices(localFileServiceName, localFS, s3FS) + if err != nil { + return err + } s.metadataFS = rfs return nil diff --git a/pkg/fileservice/errors.go b/pkg/fileservice/errors.go index 1e709e831ddfe6bc599914c4b21b1d21ba1407c2..eb3581bdcfb9d04a79b7460a4269940b6c336d74 100644 --- a/pkg/fileservice/errors.go +++ b/pkg/fileservice/errors.go @@ -20,10 +20,11 @@ import ( ) var ( - ErrFileNotFound = errors.New("file not found") - ErrFileExisted = errors.New("file existed") - ErrUnexpectedEOF = io.ErrUnexpectedEOF - ErrSizeNotMatch = errors.New("size not match") - ErrEmptyRange = errors.New("empty range") - ErrEmptyVector = errors.New("empty vector") + ErrFileNotFound = errors.New("file not found") + ErrFileExisted = errors.New("file existed") + ErrUnexpectedEOF = io.ErrUnexpectedEOF + ErrSizeNotMatch = errors.New("size not match") + ErrEmptyRange = errors.New("empty range") + ErrEmptyVector = errors.New("empty vector") + ErrDuplicatedName = errors.New("duplicated name") ) diff --git a/pkg/fileservice/file_service.go b/pkg/fileservice/file_service.go index 0f5b55efdb13b9ced21e2fe68d60a9da26194476..bed8ad113fca4691524ed883e2b0ff2147aa7a91 100644 --- a/pkg/fileservice/file_service.go +++ b/pkg/fileservice/file_service.go @@ -22,6 +22,7 @@ import ( // FileService is a write-once file system type FileService interface { // Name is file service's name + // service name is case-insensitive Name() string // Write writes a new file @@ -46,12 +47,16 @@ type FileService interface { } type IOVector struct { - // path to file, '/' separated - // add a file service name prefix to select different service - // for example, 's3:a/b/c' refer to the path 'a/b/c' in S3 + // FilePath indicates where to find the file + // a path has two parts, service name and file name, separated by ':' + // service name is optional, if omitted, the receiver FileService will use the default name of the service + // file name parts are separated by '/' + // valid characters in file names are intersection of valid characters of S3 and native file systems + // example: + // s3:a/b/c S3:a/b/c represents the same file 'a/b/c' located in 'S3' service FilePath string // io entries - // empty entry not allowed + // empty Entries not allowed Entries []IOEntry } diff --git a/pkg/fileservice/file_service_test.go b/pkg/fileservice/file_service_test.go index fca5712f620f2364e9a656934b0d2d0b28e067c3..c440d3eb2be42337d2d8a48a5eac25b4ed53c906 100644 --- a/pkg/fileservice/file_service_test.go +++ b/pkg/fileservice/file_service_test.go @@ -448,7 +448,7 @@ func testFileService( fs := newFS() err := fs.Write(ctx, IOVector{ - FilePath: fs.Name() + ":foo", + FilePath: joinPath(fs.Name(), "foo"), Entries: []IOEntry{ { Size: 4, @@ -470,6 +470,30 @@ func testFileService( assert.Nil(t, err) assert.Equal(t, []byte("1234"), vec.Entries[0].Data) + vec = IOVector{ + FilePath: joinPath(strings.ToLower(fs.Name()), "foo"), + Entries: []IOEntry{ + { + Size: -1, + }, + }, + } + err = fs.Read(ctx, &vec) + assert.Nil(t, err) + assert.Equal(t, []byte("1234"), vec.Entries[0].Data) + + vec = IOVector{ + FilePath: joinPath(strings.ToUpper(fs.Name()), "foo"), + Entries: []IOEntry{ + { + Size: -1, + }, + }, + } + err = fs.Read(ctx, &vec) + assert.Nil(t, err) + assert.Equal(t, []byte("1234"), vec.Entries[0].Data) + }) } diff --git a/pkg/fileservice/file_services.go b/pkg/fileservice/file_services.go index e8fd6515777c3301a8d4551735ad74abc23b4bc4..3fc7de2f275c307aba5176be9c61a67fe2dccace 100644 --- a/pkg/fileservice/file_services.go +++ b/pkg/fileservice/file_services.go @@ -17,6 +17,7 @@ package fileservice import ( "context" "fmt" + "strings" ) type FileServices struct { @@ -24,19 +25,19 @@ type FileServices struct { mappings map[string]FileService } -func NewFileServices(defaultName string, fss ...FileService) *FileServices { +func NewFileServices(defaultName string, fss ...FileService) (*FileServices, error) { f := &FileServices{ defaultName: defaultName, mappings: make(map[string]FileService), } for _, fs := range fss { - name := fs.Name() + name := strings.ToLower(fs.Name()) if _, ok := f.mappings[name]; ok { - panic(fmt.Errorf("duplicated file service name: %s", name)) + return nil, fmt.Errorf("%w: %s", ErrDuplicatedName, name) } f.mappings[name] = fs } - return f + return f, nil } var _ FileService = &FileServices{} diff --git a/pkg/fileservice/file_services_test.go b/pkg/fileservice/file_services_test.go index 0bbfea8568be5b4223283ea4efc6e988559161b5..367e4291e925cd50313b9793bb95253f1ae48270 100644 --- a/pkg/fileservice/file_services_test.go +++ b/pkg/fileservice/file_services_test.go @@ -26,7 +26,18 @@ func TestFileServices(t *testing.T) { dir := t.TempDir() fs, err := NewLocalFS("local", dir, 0) assert.Nil(t, err) - return NewFileServices("local", fs) + fs2, err := NewFileServices("local", fs) + assert.Nil(t, err) + return fs2 }) }) } + +func TestFileServicesNameCaseInsensitive(t *testing.T) { + fs1, err := NewMemoryFS("foo") + assert.Nil(t, err) + fs2, err := NewMemoryFS("FOO") + assert.Nil(t, err) + _, err = NewFileServices(fs1.Name(), fs1, fs2) + assert.ErrorIs(t, err, ErrDuplicatedName) +} diff --git a/pkg/fileservice/get.go b/pkg/fileservice/get.go index b337f7423fd990d92b5e9f13df90f1c36257802a..5899c1d5e27da8347587e3444692df32b0150f4e 100644 --- a/pkg/fileservice/get.go +++ b/pkg/fileservice/get.go @@ -14,11 +14,15 @@ package fileservice -import "fmt" +import ( + "fmt" + "strings" +) func Get[T any](fs FileService, name string) (res T, err error) { + lowerName := strings.ToLower(name) if fs, ok := fs.(*FileServices); ok { - f, ok := fs.mappings[name] + f, ok := fs.mappings[lowerName] if !ok { err = fmt.Errorf("file service not found: %s", name) return @@ -36,7 +40,7 @@ func Get[T any](fs FileService, name string) (res T, err error) { err = fmt.Errorf("%T does not implement %T", fs, res) return } - if fs.Name() != name { + if !strings.EqualFold(fs.Name(), lowerName) { err = fmt.Errorf("file service name not match, expecting %s, got %s", name, fs.Name()) return } diff --git a/pkg/fileservice/path.go b/pkg/fileservice/path.go index fb034115352d56f04a507e4fd833c0d96e9cd304..251289b8e34c3c0309e828e711191dfdfe8195e6 100644 --- a/pkg/fileservice/path.go +++ b/pkg/fileservice/path.go @@ -19,13 +19,25 @@ import ( "strings" ) +const ServiceNameSeparator = ":" + func splitPath(serviceName string, path string) (string, string, error) { - parts := strings.SplitN(path, ":", 2) + parts := strings.SplitN(path, ServiceNameSeparator, 2) if len(parts) == 2 { - if serviceName != "" && parts[0] != "" && parts[0] != serviceName { + if serviceName != "" && + parts[0] != "" && + !strings.EqualFold(parts[0], serviceName) { return "", "", fmt.Errorf("wrong file service name, expecting %s, got %s", serviceName, parts[0]) } return parts[0], parts[1], nil } return "", parts[0], nil } + +func joinPath(serviceName string, path string) string { + buf := new(strings.Builder) + buf.WriteString(serviceName) + buf.WriteString(ServiceNameSeparator) + buf.WriteString(path) + return buf.String() +}