11package filesystem
22
33import (
4+ fs2 "io/fs"
5+ "os"
46 "path/filepath"
5- "strings "
7+ "slices "
68 "sync"
79 "sync/atomic"
810 "time"
911
1012 "emperror.dev/errors"
1113 "github.com/apex/log"
12- "github.com/karrick/godirwalk "
14+ "golang.org/x/sys/unix "
1315)
1416
1517type SpaceCheckingOpts struct {
1618 AllowStaleResponse bool
1719}
1820
21+ // TODO: can this be replaced with some sort of atomic? Like atomic.Pointer?
1922type usageLookupTime struct {
2023 sync.RWMutex
2124 value time.Time
@@ -36,12 +39,13 @@ func (ult *usageLookupTime) Get() time.Time {
3639 return ult .value
3740}
3841
39- // Returns the maximum amount of disk space that this Filesystem instance is allowed to use.
42+ // MaxDisk returns the maximum amount of disk space that this Filesystem
43+ // instance is allowed to use.
4044func (fs * Filesystem ) MaxDisk () int64 {
4145 return atomic .LoadInt64 (& fs .diskLimit )
4246}
4347
44- // Sets the disk space limit for this Filesystem instance.
48+ // SetDiskLimit sets the disk space limit for this Filesystem instance.
4549func (fs * Filesystem ) SetDiskLimit (i int64 ) {
4650 atomic .SwapInt64 (& fs .diskLimit , i )
4751}
@@ -66,7 +70,7 @@ func (fs *Filesystem) HasSpaceErr(allowStaleValue bool) error {
6670func (fs * Filesystem ) HasSpaceAvailable (allowStaleValue bool ) bool {
6771 size , err := fs .DiskUsage (allowStaleValue )
6872 if err != nil {
69- log .WithField ("root" , fs .root ).WithField ("error" , err ).Warn ("failed to determine root fs directory size" )
73+ log .WithField ("root" , fs .Path () ).WithField ("error" , err ).Warn ("failed to determine root fs directory size" )
7074 }
7175
7276 // If space is -1 or 0 just return true, means they're allowed unlimited.
@@ -115,7 +119,7 @@ func (fs *Filesystem) DiskUsage(allowStaleValue bool) (int64, error) {
115119 // currently performing a lookup, just do the disk usage calculation in the background.
116120 go func (fs * Filesystem ) {
117121 if _ , err := fs .updateCachedDiskUsage (); err != nil {
118- log .WithField ("root" , fs .root ).WithField ("error" , err ).Warn ("failed to update fs disk usage from within routine" )
122+ log .WithField ("root" , fs .rootPath ).WithField ("error" , err ).Warn ("failed to update fs disk usage from within routine" )
119123 }
120124 }(fs )
121125 }
@@ -155,37 +159,55 @@ func (fs *Filesystem) updateCachedDiskUsage() (int64, error) {
155159 return size , err
156160}
157161
158- // Determines the directory size of a given location by running parallel tasks to iterate
159- // through all of the folders. Returns the size in bytes. This can be a fairly taxing operation
160- // on locations with tons of files, so it is recommended that you cache the output.
162+ // DirectorySize determines the directory size of a given location. Returns the size
163+ // in bytes. This can be a fairly taxing operation on locations with tons of files,
164+ // so it is recommended that you cache the output.
161165func (fs * Filesystem ) DirectorySize (dir string ) (int64 , error ) {
162- dir = strings . TrimLeft ( filepath . Clean ( dir ), "/" )
163- if dir != "" {
166+ dir = normalize ( dir )
167+ if dir != ". " {
164168 if _ , err := fs .root .Lstat (dir ); err != nil {
165169 return 0 , err
166170 }
167171 }
168172
173+ rt := fs .root
174+ if dir != "." {
175+ r , err := fs .root .OpenRoot (dir )
176+ if err != nil {
177+ return 0 , errors .Wrap (err , "server/filesystem: directorysize: failed to open root directory" )
178+ }
179+ defer r .Close ()
180+ rt = r
181+ }
182+
169183 var size int64
170- err := godirwalk .Walk (filepath .Join (fs .rootPath , dir ), & godirwalk.Options {
171- Unsorted : true ,
172- FollowSymbolicLinks : false ,
173- Callback : func (p string , e * godirwalk.Dirent ) error {
174- if ! e .ModeType ().IsRegular () {
184+ var links []uint64
185+
186+ err := filepath .WalkDir (rt .Name (), func (path string , d fs2.DirEntry , err error ) error {
187+ if ! d .Type ().IsRegular () {
188+ return nil
189+ }
190+
191+ st , err := d .Info ()
192+ if err != nil {
193+ if errors .Is (err , os .ErrNotExist ) {
175194 return nil
176195 }
196+ return err
197+ }
177198
178- if ! e .IsDir () {
179- st , err := fs .root .Lstat (strings .TrimLeft (strings .TrimPrefix (p , fs .rootPath ), "/" ))
180- if err != nil {
181- return errors .Wrap (err , "server/filesystem: directorysize: failed to stat file" )
182- }
183- atomic .AddInt64 (& size , st .Size ())
199+ s := st .Sys ().(* unix.Stat_t )
200+ if s .Nlink > 1 {
201+ // Hard links have the same inode number, don't add them more than once.
202+ if slices .Contains (links , s .Ino ) {
203+ return nil
184204 }
205+ links = append (links , s .Ino )
206+ }
185207
186- // todo: don't count hardlinks twice
187- return nil
188- },
208+ size += st . Size ()
209+
210+ return nil
189211 })
190212
191213 return size , errors .WrapIf (err , "server/filesystem: directorysize: failed to walk directory" )
0 commit comments