github.com/sagansystems/goofys-app@v0.19.1-0.20180410053237-b2302fdf5af9/internal/dir.go (about)

     1  // Copyright 2015 - 2017 Ka-Hing Cheung
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package internal
    16  
    17  import (
    18  	"fmt"
    19  	"sort"
    20  	"strings"
    21  	"sync"
    22  	"time"
    23  
    24  	"github.com/aws/aws-sdk-go/aws"
    25  	"github.com/aws/aws-sdk-go/service/s3"
    26  
    27  	"github.com/jacobsa/fuse"
    28  	"github.com/jacobsa/fuse/fuseops"
    29  	"github.com/jacobsa/fuse/fuseutil"
    30  )
    31  
    32  type DirInodeData struct {
    33  	// these 2 refer to readdir of the Children
    34  	lastOpenDir     *string
    35  	lastOpenDirIdx  int
    36  	seqOpenDirScore uint8
    37  	DirTime         time.Time
    38  
    39  	Children []*Inode
    40  }
    41  
    42  type DirHandleEntry struct {
    43  	Name   *string
    44  	Inode  fuseops.InodeID
    45  	Type   fuseutil.DirentType
    46  	Offset fuseops.DirOffset
    47  
    48  	Attributes   *InodeAttributes
    49  	ETag         *string
    50  	StorageClass *string
    51  }
    52  
    53  type DirHandle struct {
    54  	inode *Inode
    55  
    56  	mu         sync.Mutex // everything below is protected by mu
    57  	Entries    []*DirHandleEntry
    58  	Marker     *string
    59  	BaseOffset int
    60  }
    61  
    62  func NewDirHandle(inode *Inode) (dh *DirHandle) {
    63  	dh = &DirHandle{inode: inode}
    64  	return
    65  }
    66  
    67  func (inode *Inode) OpenDir() (dh *DirHandle) {
    68  	inode.logFuse("OpenDir")
    69  
    70  	parent := inode.Parent
    71  	if parent != nil && inode.fs.flags.TypeCacheTTL != 0 {
    72  		parent.mu.Lock()
    73  		defer parent.mu.Unlock()
    74  
    75  		num := len(parent.dir.Children)
    76  
    77  		if parent.dir.lastOpenDir == nil && num > 0 && *parent.dir.Children[0].Name == *inode.Name {
    78  			if parent.dir.seqOpenDirScore < 255 {
    79  				parent.dir.seqOpenDirScore += 1
    80  			}
    81  			// 2.1) if I open a/a, a/'s score is now 2
    82  			// ie: handle the depth first search case
    83  			if parent.dir.seqOpenDirScore >= 2 {
    84  				fuseLog.Debugf("%v in readdir mode", *parent.FullName())
    85  			}
    86  		} else if parent.dir.lastOpenDir != nil && parent.dir.lastOpenDirIdx+1 < num &&
    87  			// we are reading the next one as expected
    88  			*parent.dir.Children[parent.dir.lastOpenDirIdx+1].Name == *inode.Name &&
    89  			// check that inode positions haven't moved
    90  			*parent.dir.Children[parent.dir.lastOpenDirIdx].Name == *parent.dir.lastOpenDir {
    91  			// 2.2) if I open b/, root's score is now 2
    92  			// ie: handle the breath first search case
    93  			if parent.dir.seqOpenDirScore < 255 {
    94  				parent.dir.seqOpenDirScore++
    95  			}
    96  			parent.dir.lastOpenDirIdx += 1
    97  			if parent.dir.seqOpenDirScore == 2 {
    98  				fuseLog.Debugf("%v in readdir mode", *parent.FullName())
    99  			}
   100  		} else {
   101  			parent.dir.seqOpenDirScore = 0
   102  			parent.dir.lastOpenDirIdx = parent.findChildIdxUnlocked(*inode.Name)
   103  			if parent.dir.lastOpenDirIdx == -1 {
   104  				panic(fmt.Sprintf("%v is not under %v", *inode.Name, *parent.FullName()))
   105  			}
   106  		}
   107  
   108  		parent.dir.lastOpenDir = inode.Name
   109  		inode.mu.Lock()
   110  		defer inode.mu.Unlock()
   111  
   112  		if inode.dir.lastOpenDir == nil {
   113  			// 1) if I open a/, root's score = 1 (a is the first dir),
   114  			// so make a/'s count at 1 too
   115  			inode.dir.seqOpenDirScore = parent.dir.seqOpenDirScore
   116  			if inode.dir.seqOpenDirScore >= 2 {
   117  				fuseLog.Debugf("%v in readdir mode", *inode.FullName())
   118  			}
   119  		}
   120  	}
   121  
   122  	dh = NewDirHandle(inode)
   123  	return
   124  }
   125  
   126  // Dirents, sorted by name.
   127  type sortedDirents []*DirHandleEntry
   128  
   129  func (p sortedDirents) Len() int           { return len(p) }
   130  func (p sortedDirents) Less(i, j int) bool { return *p[i].Name < *p[j].Name }
   131  func (p sortedDirents) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }
   132  
   133  func (dh *DirHandle) listObjectsSlurp(prefix string) (resp *s3.ListObjectsOutput, err error) {
   134  	var marker *string
   135  	reqPrefix := prefix
   136  	inode := dh.inode
   137  	fs := inode.fs
   138  	if dh.inode.Parent != nil {
   139  		inode = dh.inode.Parent
   140  		reqPrefix = *fs.key(*inode.FullName())
   141  		if len(*inode.FullName()) != 0 {
   142  			reqPrefix += "/"
   143  		}
   144  		marker = fs.key(*dh.inode.FullName() + "/")
   145  	}
   146  
   147  	params := &s3.ListObjectsInput{
   148  		Bucket:       &fs.bucket,
   149  		Prefix:       &reqPrefix,
   150  		Marker:       marker,
   151  		EncodingType: aws.String("url"),
   152  	}
   153  
   154  	resp, err = fs.s3.ListObjects(params)
   155  	if err != nil {
   156  		s3Log.Errorf("ListObjects %v = %v", params, err)
   157  		return
   158  	}
   159  
   160  	num := len(resp.Contents)
   161  	if num == 0 {
   162  		return
   163  	}
   164  
   165  	dirs := make(map[*Inode]bool)
   166  	for _, obj := range resp.Contents {
   167  		baseName := (*obj.Key)[len(reqPrefix):]
   168  
   169  		slash := strings.Index(baseName, "/")
   170  		if slash != -1 {
   171  			inode.insertSubTree(baseName, obj, dirs)
   172  		}
   173  	}
   174  
   175  	for d, sealed := range dirs {
   176  		if d == dh.inode {
   177  			// never seal the current dir because that's
   178  			// handled at upper layer
   179  			continue
   180  		}
   181  
   182  		if sealed || !*resp.IsTruncated {
   183  			d.dir.DirTime = time.Now()
   184  			d.Attributes.Mtime = d.findChildMaxTime()
   185  		}
   186  	}
   187  
   188  	if *resp.IsTruncated {
   189  		obj := resp.Contents[len(resp.Contents)-1]
   190  		// if we are done listing prefix, we are good
   191  		if strings.HasPrefix(*obj.Key, prefix) {
   192  			// if we are done with all the slashes, then we are good
   193  			baseName := (*obj.Key)[len(prefix):]
   194  
   195  			for _, c := range baseName {
   196  				if c <= '/' {
   197  					// if an entry is ex: a!b, then the
   198  					// next entry could be a/foo, so we
   199  					// are not done yet.
   200  					resp = nil
   201  					break
   202  				}
   203  			}
   204  		}
   205  	}
   206  
   207  	// we only return this response if we are totally done with listing this dir
   208  	if resp != nil {
   209  		resp.IsTruncated = aws.Bool(false)
   210  		resp.NextMarker = nil
   211  	}
   212  
   213  	return
   214  }
   215  
   216  func (dh *DirHandle) listObjects(prefix string) (resp *s3.ListObjectsOutput, err error) {
   217  	errSlurpChan := make(chan error, 1)
   218  	slurpChan := make(chan s3.ListObjectsOutput, 1)
   219  	errListChan := make(chan error, 1)
   220  	listChan := make(chan s3.ListObjectsOutput, 1)
   221  
   222  	fs := dh.inode.fs
   223  
   224  	// try to list without delimiter to see if we have slurp up
   225  	// multiple directories
   226  	if dh.Marker == nil &&
   227  		fs.flags.TypeCacheTTL != 0 &&
   228  		(dh.inode.Parent != nil && dh.inode.Parent.dir.seqOpenDirScore >= 2) {
   229  		go func() {
   230  			resp, err := dh.listObjectsSlurp(prefix)
   231  			if err != nil {
   232  				errSlurpChan <- err
   233  			} else if resp != nil {
   234  				slurpChan <- *resp
   235  			} else {
   236  				errSlurpChan <- fuse.EINVAL
   237  			}
   238  		}()
   239  	} else {
   240  		errSlurpChan <- fuse.EINVAL
   241  	}
   242  
   243  	listObjectsFlat := func() {
   244  		params := &s3.ListObjectsInput{
   245  			Bucket:       &fs.bucket,
   246  			Delimiter:    aws.String("/"),
   247  			Marker:       dh.Marker,
   248  			Prefix:       &prefix,
   249  			EncodingType: aws.String("url"),
   250  		}
   251  
   252  		resp, err := fs.s3.ListObjects(params)
   253  		if err != nil {
   254  			errListChan <- err
   255  		} else {
   256  			listChan <- *resp
   257  		}
   258  	}
   259  
   260  	if !fs.flags.Cheap {
   261  		// invoke the fallback in parallel if desired
   262  		go listObjectsFlat()
   263  	}
   264  
   265  	// first see if we get anything from the slurp
   266  	select {
   267  	case resp := <-slurpChan:
   268  		return &resp, nil
   269  	case err = <-errSlurpChan:
   270  	}
   271  
   272  	if fs.flags.Cheap {
   273  		listObjectsFlat()
   274  	}
   275  
   276  	// if we got an error (which may mean slurp is not applicable,
   277  	// wait for regular list
   278  	select {
   279  	case resp := <-listChan:
   280  		return &resp, nil
   281  	case err = <-errListChan:
   282  		return
   283  	}
   284  }
   285  
   286  func objectToDirEntry(fs *Goofys, obj *s3.Object, name string, isDir bool) (en *DirHandleEntry) {
   287  	if isDir {
   288  		en = &DirHandleEntry{
   289  			Name:       &name,
   290  			Type:       fuseutil.DT_Directory,
   291  			Attributes: &fs.rootAttrs,
   292  		}
   293  	} else {
   294  		en = &DirHandleEntry{
   295  			Name: &name,
   296  			Type: fuseutil.DT_File,
   297  			Attributes: &InodeAttributes{
   298  				Size:  uint64(*obj.Size),
   299  				Mtime: *obj.LastModified,
   300  			},
   301  			ETag:         obj.ETag,
   302  			StorageClass: obj.StorageClass,
   303  		}
   304  	}
   305  
   306  	return
   307  }
   308  
   309  // LOCKS_REQUIRED(dh.mu)
   310  func (dh *DirHandle) ReadDir(offset fuseops.DirOffset) (en *DirHandleEntry, err error) {
   311  	// If the request is for offset zero, we assume that either this is the first
   312  	// call or rewinddir has been called. Reset state.
   313  	if offset == 0 {
   314  		dh.Entries = nil
   315  	}
   316  
   317  	en, ok := dh.inode.readDirFromCache(offset)
   318  	if ok {
   319  		return
   320  	}
   321  
   322  	fs := dh.inode.fs
   323  
   324  	if offset == 0 {
   325  		// content from the cache expired (otherwise we would
   326  		// have returned from above, so remove all of it
   327  		dh.inode.dir.Children = nil
   328  
   329  		en = &DirHandleEntry{
   330  			Name:       aws.String("."),
   331  			Type:       fuseutil.DT_Directory,
   332  			Attributes: &fs.rootAttrs,
   333  			Offset:     1,
   334  		}
   335  		return
   336  	} else if offset == 1 {
   337  		en = &DirHandleEntry{
   338  			Name:       aws.String(".."),
   339  			Type:       fuseutil.DT_Directory,
   340  			Attributes: &fs.rootAttrs,
   341  			Offset:     2,
   342  		}
   343  		return
   344  	}
   345  
   346  	i := int(offset) - dh.BaseOffset - 2
   347  	if i < 0 {
   348  		panic(fmt.Sprintf("invalid offset %v, base=%v", offset, dh.BaseOffset))
   349  	}
   350  
   351  	if i >= len(dh.Entries) {
   352  		if dh.Marker != nil {
   353  			// we need to fetch the next page
   354  			dh.Entries = nil
   355  			dh.BaseOffset += i
   356  			i = 0
   357  		}
   358  	}
   359  
   360  	if dh.Entries == nil {
   361  		// try not to hold the lock when we make the request
   362  		dh.mu.Unlock()
   363  
   364  		prefix := *fs.key(*dh.inode.FullName())
   365  		if len(*dh.inode.FullName()) != 0 {
   366  			prefix += "/"
   367  		}
   368  
   369  		resp, err := dh.listObjects(prefix)
   370  		if err != nil {
   371  			dh.mu.Lock()
   372  			return nil, mapAwsError(err)
   373  		}
   374  
   375  		s3Log.Debug(resp)
   376  		dh.mu.Lock()
   377  
   378  		dh.Entries = make([]*DirHandleEntry, 0, len(resp.CommonPrefixes)+len(resp.Contents))
   379  
   380  		// this is only returned for non-slurped responses
   381  		for _, dir := range resp.CommonPrefixes {
   382  			// strip trailing /
   383  			dirName := (*dir.Prefix)[0 : len(*dir.Prefix)-1]
   384  			// strip previous prefix
   385  			dirName = dirName[len(*resp.Prefix):]
   386  			if len(dirName) == 0 {
   387  				continue
   388  			}
   389  			en = &DirHandleEntry{
   390  				Name:       &dirName,
   391  				Type:       fuseutil.DT_Directory,
   392  				Attributes: &fs.rootAttrs,
   393  			}
   394  
   395  			dh.Entries = append(dh.Entries, en)
   396  		}
   397  
   398  		lastDir := ""
   399  		for _, obj := range resp.Contents {
   400  			if !strings.HasPrefix(*obj.Key, prefix) {
   401  				// other slurped objects that we cached
   402  				continue
   403  			}
   404  
   405  			baseName := (*obj.Key)[len(prefix):]
   406  
   407  			slash := strings.Index(baseName, "/")
   408  			if slash == -1 {
   409  				if len(baseName) == 0 {
   410  					// shouldn't happen
   411  					continue
   412  				}
   413  				dh.Entries = append(dh.Entries,
   414  					objectToDirEntry(fs, obj, baseName, false))
   415  			} else {
   416  				// this is a slurped up object which
   417  				// was already cached, unless it's a
   418  				// directory right under this dir that
   419  				// we need to return
   420  				dirName := baseName[:slash]
   421  				if dirName != lastDir && lastDir != "" {
   422  					// make a copy so we can take the address
   423  					dir := lastDir
   424  					en := &DirHandleEntry{
   425  						Name:       &dir,
   426  						Type:       fuseutil.DT_Directory,
   427  						Attributes: &fs.rootAttrs,
   428  					}
   429  					dh.Entries = append(dh.Entries, en)
   430  				}
   431  				lastDir = dirName
   432  			}
   433  		}
   434  		if lastDir != "" {
   435  			en := &DirHandleEntry{
   436  				Name:       &lastDir,
   437  				Type:       fuseutil.DT_Directory,
   438  				Attributes: &fs.rootAttrs,
   439  			}
   440  			dh.Entries = append(dh.Entries, en)
   441  		}
   442  
   443  		sort.Sort(sortedDirents(dh.Entries))
   444  
   445  		// Fix up offset fields.
   446  		for i := 0; i < len(dh.Entries); i++ {
   447  			en := dh.Entries[i]
   448  			// offset is 1 based, also need to account for "." and ".."
   449  			en.Offset = fuseops.DirOffset(i+dh.BaseOffset) + 1 + 2
   450  		}
   451  
   452  		if *resp.IsTruncated {
   453  			dh.Marker = resp.NextMarker
   454  		} else {
   455  			dh.Marker = nil
   456  		}
   457  	}
   458  
   459  	if i == len(dh.Entries) {
   460  		// we've reached the end
   461  		return nil, nil
   462  	} else if i > len(dh.Entries) {
   463  		return nil, fuse.EINVAL
   464  	}
   465  
   466  	return dh.Entries[i], nil
   467  }
   468  
   469  func (dh *DirHandle) CloseDir() error {
   470  	return nil
   471  }