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 }