@@ -7,40 +7,82 @@ import (
77 "io/fs"
88 "os"
99 "path/filepath"
10+ "strings"
1011
1112 "github.com/ipni/storetheindex/fsutil"
1213)
1314
1415// Local is a file store that stores files in the local file system.
1516type Local struct {
16- basePath string
17+ basePath string
18+ pathSplit []int
1719}
1820
19- func NewLocal (basePath string ) (* Local , error ) {
21+ func NewLocal (basePath string , options ... LocalOption ) (* Local , error ) {
2022 if ! filepath .IsAbs (basePath ) {
2123 return nil , errors .New ("base path must be absolute" )
2224 }
25+
2326 err := fsutil .DirWritable (basePath )
2427 if err != nil {
2528 return nil , err
2629 }
30+
31+ opts , err := getLocalOpts (options )
32+ if err != nil {
33+ return nil , err
34+ }
35+
2736 return & Local {
28- basePath : basePath ,
37+ basePath : basePath ,
38+ pathSplit : opts .pathSplit ,
2939 }, nil
3040}
3141
42+ // fsPath returns the filesystem path of a given object path
43+ // based on a given basePath with path splitting criteria applied.
44+ func (l * Local ) fsPath (basePath , relPath string ) string {
45+ fsDir , fsName := filepath .Split (filepath .FromSlash (relPath ))
46+
47+ pathSegments := make ([]string , 0 , len (l .pathSplit )+ 3 )
48+ pathSegments = append (pathSegments , basePath , fsDir )
49+
50+ pathSegments = l .appendFnamePathSegments (fsName , pathSegments )
51+
52+ return filepath .Join (pathSegments ... )
53+ }
54+
55+ func (l * Local ) appendFnamePathSegments (fileName string , pathSegments []string ) []string {
56+ fsNameNoExt := fileName
57+ if dotPos := strings .IndexByte (fileName , '.' ); dotPos > 0 {
58+ fsNameNoExt = fileName [:dotPos ]
59+ }
60+
61+ splitPos := 0
62+ for _ , split := range l .pathSplit {
63+ splitPos += split
64+ if splitPos > len (fsNameNoExt ) {
65+ break
66+ }
67+
68+ pathSegments = append (pathSegments , fsNameNoExt [splitPos - split :splitPos ])
69+ }
70+
71+ pathSegments = append (pathSegments , fileName )
72+
73+ return pathSegments
74+ }
75+
3276func (l * Local ) Delete (ctx context.Context , relPath string ) error {
33- err := os .Remove (filepath . Join (l .basePath , filepath . FromSlash ( relPath ) ))
77+ err := os .Remove (l . fsPath (l .basePath , relPath ))
3478 if err != nil && ! errors .Is (err , os .ErrNotExist ) {
3579 return err
3680 }
3781 return nil
3882}
3983
4084func (l * Local ) Get (ctx context.Context , relPath string ) (* File , io.ReadCloser , error ) {
41- absPath := filepath .Join (l .basePath , filepath .FromSlash (relPath ))
42-
43- f , err := os .Open (absPath )
85+ f , err := os .Open (l .fsPath (l .basePath , relPath ))
4486 if err != nil {
4587 if os .IsNotExist (err ) {
4688 return nil , nil , fs .ErrNotExist
@@ -67,8 +109,7 @@ func (l *Local) Get(ctx context.Context, relPath string) (*File, io.ReadCloser,
67109}
68110
69111func (l * Local ) Head (ctx context.Context , relPath string ) (* File , error ) {
70- absPath := filepath .Join (l .basePath , filepath .FromSlash (relPath ))
71- fi , err := os .Stat (absPath )
112+ fi , err := os .Stat (l .fsPath (l .basePath , relPath ))
72113 if err != nil {
73114 if errors .Is (err , os .ErrNotExist ) {
74115 return nil , fs .ErrNotExist
@@ -95,7 +136,23 @@ func (l *Local) List(ctx context.Context, relPath string, recursive bool) (<-cha
95136 defer close (e )
96137 defer close (c )
97138
98- absPath := filepath .Join (l .basePath , filepath .FromSlash (relPath ))
139+ // The relPath may either be a path to a file or a path prefix,
140+ // those cases must be handled separately due to pathSplit that only
141+ // considers the filename as a splittable segment.
142+ if stat , err := os .Stat (l .fsPath (l .basePath , relPath )); err == nil && stat .Mode ().IsRegular () {
143+ // relPath points to a file - emit that one and exit
144+ c <- & File {
145+ Modified : stat .ModTime (),
146+ Path : relPath ,
147+ Size : stat .Size (),
148+ }
149+
150+ return
151+ }
152+
153+ // relPath must be treated as a directory prefix
154+ absPath := filepath .Join (l .basePath , relPath )
155+
99156 e <- filepath .WalkDir (absPath , func (path string , d fs.DirEntry , err error ) error {
100157 if err != nil {
101158 if errors .Is (err , os .ErrNotExist ) {
@@ -106,9 +163,18 @@ func (l *Local) List(ctx context.Context, relPath string, recursive bool) (<-cha
106163 }
107164
108165 if d .IsDir () {
109- if ! recursive && path != absPath {
166+ if recursive || len (l .pathSplit ) > 0 {
167+ // For both recursive scan and filepath split, keep descending
168+ // to find files in sub-directories
169+ return nil
170+ }
171+
172+ if path != absPath {
173+ // Without path split only a flat structure allowed,
174+ // skip any sub-directories for faster iteration
110175 return fs .SkipDir
111176 }
177+
112178 return nil
113179 }
114180
@@ -122,15 +188,43 @@ func (l *Local) List(ctx context.Context, relPath string, recursive bool) (<-cha
122188 return err
123189 }
124190
125- relFilePath , err := filepath .Rel (l .basePath , path )
126- if err != nil {
127- return err
128- }
129-
130191 f := & File {
131192 Modified : fi .ModTime (),
132- Path : filepath .ToSlash (relFilePath ),
133- Size : fi .Size (),
193+ // Path: filepath.ToSlash(relFilePath),
194+ Size : fi .Size (),
195+ }
196+
197+ if len (l .pathSplit ) > 0 {
198+ // Before emitting file entry, verify that the path is correct according
199+ // to path split rules
200+ fileName := filepath .Base (path )
201+ expectedFileSubPath := l .fsPath ("" , fileName )
202+
203+ relAbsPath , err := filepath .Rel (absPath , path )
204+ if err != nil {
205+ return err
206+ }
207+
208+ if prefix , found := strings .CutSuffix (relAbsPath , expectedFileSubPath ); ! found {
209+ // Skip the file, path structure does not match
210+ return nil
211+ } else if ! recursive && prefix != "" {
212+ // Structure matches but is in sub-folder and not recursive mode, skip that one
213+ return nil
214+ } else if prefix != "" && prefix [len (prefix )- 1 ] != filepath .Separator {
215+ // Corner case - the prefix path must align with the path separator boundary
216+ return nil
217+ } else {
218+ // All looks good, flatten adjust the final path by removing sub-folder structure
219+ f .Path = filepath .ToSlash (filepath .Join (relPath , prefix , fileName ))
220+ }
221+ } else {
222+ relFilePath , err := filepath .Rel (l .basePath , path )
223+ if err != nil {
224+ return err
225+ }
226+
227+ f .Path = filepath .ToSlash (relFilePath )
134228 }
135229
136230 select {
@@ -146,11 +240,10 @@ func (l *Local) List(ctx context.Context, relPath string, recursive bool) (<-cha
146240}
147241
148242func (l * Local ) Put (ctx context.Context , relPath string , r io.Reader ) (* File , error ) {
149- absPath := filepath . Join (l .basePath , filepath . FromSlash ( relPath ) )
243+ absPath := l . fsPath (l .basePath , relPath )
150244
151- dir , _ := filepath .Split (relPath )
152- if dir != "" {
153- err := os .MkdirAll (filepath .Dir (absPath ), 0755 )
245+ if dir := filepath .Dir (absPath ); dir != "" {
246+ err := os .MkdirAll (dir , 0755 )
154247 if err != nil {
155248 return nil , err
156249 }
0 commit comments