github.com/cockroachdb/pebble@v1.1.5/vfs/mem_fs.go (about) 1 // Copyright 2012 The LevelDB-Go and Pebble Authors. All rights reserved. Use 2 // of this source code is governed by a BSD-style license that can be found in 3 // the LICENSE file. 4 5 package vfs // import "github.com/cockroachdb/pebble/vfs" 6 7 import ( 8 "bytes" 9 "fmt" 10 "io" 11 "os" 12 "path" 13 "sort" 14 "strings" 15 "sync" 16 "sync/atomic" 17 "syscall" 18 "time" 19 20 "github.com/cockroachdb/errors" 21 "github.com/cockroachdb/errors/oserror" 22 "github.com/cockroachdb/pebble/internal/invariants" 23 ) 24 25 const sep = "/" 26 27 // NewMem returns a new memory-backed FS implementation. 28 func NewMem() *MemFS { 29 return &MemFS{ 30 root: newRootMemNode(), 31 } 32 } 33 34 // NewStrictMem returns a "strict" memory-backed FS implementation. The behaviour is strict wrt 35 // needing a Sync() call on files or directories for the state changes to be finalized. Any 36 // changes that are not finalized are visible to reads until MemFS.ResetToSyncedState() is called, 37 // at which point they are discarded and no longer visible. 38 // 39 // Expected usage: 40 // 41 // strictFS := NewStrictMem() 42 // db := Open(..., &Options{FS: strictFS}) 43 // // Do and commit various operations. 44 // ... 45 // // Prevent any more changes to finalized state. 46 // strictFS.SetIgnoreSyncs(true) 47 // // This will finish any ongoing background flushes, compactions but none of these writes will 48 // // be finalized since syncs are being ignored. 49 // db.Close() 50 // // Discard unsynced state. 51 // strictFS.ResetToSyncedState() 52 // // Allow changes to finalized state. 53 // strictFS.SetIgnoreSyncs(false) 54 // // Open the DB. This DB should have the same state as if the earlier strictFS operations and 55 // // db.Close() were not called. 56 // db := Open(..., &Options{FS: strictFS}) 57 func NewStrictMem() *MemFS { 58 return &MemFS{ 59 root: newRootMemNode(), 60 strict: true, 61 } 62 } 63 64 // NewMemFile returns a memory-backed File implementation. The memory-backed 65 // file takes ownership of data. 66 func NewMemFile(data []byte) File { 67 n := &memNode{} 68 n.refs.Store(1) 69 n.mu.data = data 70 n.mu.modTime = time.Now() 71 return &memFile{ 72 n: n, 73 read: true, 74 } 75 } 76 77 // MemFS implements FS. 78 type MemFS struct { 79 mu sync.Mutex 80 root *memNode 81 82 // lockFiles holds a map of open file locks. Presence in this map indicates 83 // a file lock is currently held. Keys are strings holding the path of the 84 // locked file. The stored value is untyped and unused; only presence of 85 // the key within the map is significant. 86 lockedFiles sync.Map 87 strict bool 88 ignoreSyncs bool 89 // Windows has peculiar semantics with respect to hard links and deleting 90 // open files. In tests meant to exercise this behavior, this flag can be 91 // set to error if removing an open file. 92 windowsSemantics bool 93 } 94 95 var _ FS = &MemFS{} 96 97 // UseWindowsSemantics configures whether the MemFS implements Windows-style 98 // semantics, in particular with respect to whether any of an open file's links 99 // may be removed. Windows semantics default to off. 100 func (y *MemFS) UseWindowsSemantics(windowsSemantics bool) { 101 y.mu.Lock() 102 defer y.mu.Unlock() 103 y.windowsSemantics = windowsSemantics 104 } 105 106 // String dumps the contents of the MemFS. 107 func (y *MemFS) String() string { 108 y.mu.Lock() 109 defer y.mu.Unlock() 110 111 s := new(bytes.Buffer) 112 y.root.dump(s, 0) 113 return s.String() 114 } 115 116 // SetIgnoreSyncs sets the MemFS.ignoreSyncs field. See the usage comment with NewStrictMem() for 117 // details. 118 func (y *MemFS) SetIgnoreSyncs(ignoreSyncs bool) { 119 y.mu.Lock() 120 if !y.strict { 121 // noop 122 return 123 } 124 y.ignoreSyncs = ignoreSyncs 125 y.mu.Unlock() 126 } 127 128 // ResetToSyncedState discards state in the FS that is not synced. See the usage comment with 129 // NewStrictMem() for details. 130 func (y *MemFS) ResetToSyncedState() { 131 if !y.strict { 132 // noop 133 return 134 } 135 y.mu.Lock() 136 y.root.resetToSyncedState() 137 y.mu.Unlock() 138 } 139 140 // walk walks the directory tree for the fullname, calling f at each step. If 141 // f returns an error, the walk will be aborted and return that same error. 142 // 143 // Each walk is atomic: y's mutex is held for the entire operation, including 144 // all calls to f. 145 // 146 // dir is the directory at that step, frag is the name fragment, and final is 147 // whether it is the final step. For example, walking "/foo/bar/x" will result 148 // in 3 calls to f: 149 // - "/", "foo", false 150 // - "/foo/", "bar", false 151 // - "/foo/bar/", "x", true 152 // 153 // Similarly, walking "/y/z/", with a trailing slash, will result in 3 calls to f: 154 // - "/", "y", false 155 // - "/y/", "z", false 156 // - "/y/z/", "", true 157 func (y *MemFS) walk(fullname string, f func(dir *memNode, frag string, final bool) error) error { 158 y.mu.Lock() 159 defer y.mu.Unlock() 160 161 // For memfs, the current working directory is the same as the root directory, 162 // so we strip off any leading "/"s to make fullname a relative path, and 163 // the walk starts at y.root. 164 for len(fullname) > 0 && fullname[0] == sep[0] { 165 fullname = fullname[1:] 166 } 167 if fullname == "." { 168 fullname = "" 169 } 170 dir := y.root 171 172 for { 173 frag, remaining := fullname, "" 174 i := strings.IndexRune(fullname, rune(sep[0])) 175 final := i < 0 176 if !final { 177 frag, remaining = fullname[:i], fullname[i+1:] 178 for len(remaining) > 0 && remaining[0] == sep[0] { 179 remaining = remaining[1:] 180 } 181 } 182 if err := f(dir, frag, final); err != nil { 183 return err 184 } 185 if final { 186 break 187 } 188 child := dir.children[frag] 189 if child == nil { 190 return &os.PathError{ 191 Op: "open", 192 Path: fullname, 193 Err: oserror.ErrNotExist, 194 } 195 } 196 if !child.isDir { 197 return &os.PathError{ 198 Op: "open", 199 Path: fullname, 200 Err: errors.New("not a directory"), 201 } 202 } 203 dir, fullname = child, remaining 204 } 205 return nil 206 } 207 208 // Create implements FS.Create. 209 func (y *MemFS) Create(fullname string) (File, error) { 210 var ret *memFile 211 err := y.walk(fullname, func(dir *memNode, frag string, final bool) error { 212 if final { 213 if frag == "" { 214 return errors.New("pebble/vfs: empty file name") 215 } 216 n := &memNode{name: frag} 217 dir.children[frag] = n 218 ret = &memFile{ 219 n: n, 220 fs: y, 221 read: true, 222 write: true, 223 } 224 } 225 return nil 226 }) 227 if err != nil { 228 return nil, err 229 } 230 ret.n.refs.Add(1) 231 return ret, nil 232 } 233 234 // Link implements FS.Link. 235 func (y *MemFS) Link(oldname, newname string) error { 236 var n *memNode 237 err := y.walk(oldname, func(dir *memNode, frag string, final bool) error { 238 if final { 239 if frag == "" { 240 return errors.New("pebble/vfs: empty file name") 241 } 242 n = dir.children[frag] 243 } 244 return nil 245 }) 246 if err != nil { 247 return err 248 } 249 if n == nil { 250 return &os.LinkError{ 251 Op: "link", 252 Old: oldname, 253 New: newname, 254 Err: oserror.ErrNotExist, 255 } 256 } 257 return y.walk(newname, func(dir *memNode, frag string, final bool) error { 258 if final { 259 if frag == "" { 260 return errors.New("pebble/vfs: empty file name") 261 } 262 if _, ok := dir.children[frag]; ok { 263 return &os.LinkError{ 264 Op: "link", 265 Old: oldname, 266 New: newname, 267 Err: oserror.ErrExist, 268 } 269 } 270 dir.children[frag] = n 271 } 272 return nil 273 }) 274 } 275 276 func (y *MemFS) open(fullname string, openForWrite bool) (File, error) { 277 var ret *memFile 278 err := y.walk(fullname, func(dir *memNode, frag string, final bool) error { 279 if final { 280 if frag == "" { 281 ret = &memFile{ 282 n: dir, 283 fs: y, 284 } 285 return nil 286 } 287 if n := dir.children[frag]; n != nil { 288 ret = &memFile{ 289 n: n, 290 fs: y, 291 read: true, 292 write: openForWrite, 293 } 294 } 295 } 296 return nil 297 }) 298 if err != nil { 299 return nil, err 300 } 301 if ret == nil { 302 return nil, &os.PathError{ 303 Op: "open", 304 Path: fullname, 305 Err: oserror.ErrNotExist, 306 } 307 } 308 ret.n.refs.Add(1) 309 return ret, nil 310 } 311 312 // Open implements FS.Open. 313 func (y *MemFS) Open(fullname string, opts ...OpenOption) (File, error) { 314 return y.open(fullname, false /* openForWrite */) 315 } 316 317 // OpenReadWrite implements FS.OpenReadWrite. 318 func (y *MemFS) OpenReadWrite(fullname string, opts ...OpenOption) (File, error) { 319 f, err := y.open(fullname, true /* openForWrite */) 320 pathErr, ok := err.(*os.PathError) 321 if ok && pathErr.Err == oserror.ErrNotExist { 322 return y.Create(fullname) 323 } 324 return f, err 325 } 326 327 // OpenDir implements FS.OpenDir. 328 func (y *MemFS) OpenDir(fullname string) (File, error) { 329 return y.open(fullname, false /* openForWrite */) 330 } 331 332 // Remove implements FS.Remove. 333 func (y *MemFS) Remove(fullname string) error { 334 return y.walk(fullname, func(dir *memNode, frag string, final bool) error { 335 if final { 336 if frag == "" { 337 return errors.New("pebble/vfs: empty file name") 338 } 339 child, ok := dir.children[frag] 340 if !ok { 341 return oserror.ErrNotExist 342 } 343 if y.windowsSemantics { 344 // Disallow removal of open files/directories which implements 345 // Windows semantics. This ensures that we don't regress in the 346 // ordering of operations and try to remove a file while it is 347 // still open. 348 if n := child.refs.Load(); n > 0 { 349 return oserror.ErrInvalid 350 } 351 } 352 if len(child.children) > 0 { 353 return errNotEmpty 354 } 355 delete(dir.children, frag) 356 } 357 return nil 358 }) 359 } 360 361 // RemoveAll implements FS.RemoveAll. 362 func (y *MemFS) RemoveAll(fullname string) error { 363 err := y.walk(fullname, func(dir *memNode, frag string, final bool) error { 364 if final { 365 if frag == "" { 366 return errors.New("pebble/vfs: empty file name") 367 } 368 _, ok := dir.children[frag] 369 if !ok { 370 return nil 371 } 372 delete(dir.children, frag) 373 } 374 return nil 375 }) 376 // Match os.RemoveAll which returns a nil error even if the parent 377 // directories don't exist. 378 if oserror.IsNotExist(err) { 379 err = nil 380 } 381 return err 382 } 383 384 // Rename implements FS.Rename. 385 func (y *MemFS) Rename(oldname, newname string) error { 386 var n *memNode 387 err := y.walk(oldname, func(dir *memNode, frag string, final bool) error { 388 if final { 389 if frag == "" { 390 return errors.New("pebble/vfs: empty file name") 391 } 392 n = dir.children[frag] 393 delete(dir.children, frag) 394 } 395 return nil 396 }) 397 if err != nil { 398 return err 399 } 400 if n == nil { 401 return &os.PathError{ 402 Op: "open", 403 Path: oldname, 404 Err: oserror.ErrNotExist, 405 } 406 } 407 return y.walk(newname, func(dir *memNode, frag string, final bool) error { 408 if final { 409 if frag == "" { 410 return errors.New("pebble/vfs: empty file name") 411 } 412 dir.children[frag] = n 413 n.name = frag 414 } 415 return nil 416 }) 417 } 418 419 // ReuseForWrite implements FS.ReuseForWrite. 420 func (y *MemFS) ReuseForWrite(oldname, newname string) (File, error) { 421 if err := y.Rename(oldname, newname); err != nil { 422 return nil, err 423 } 424 f, err := y.Open(newname) 425 if err != nil { 426 return nil, err 427 } 428 y.mu.Lock() 429 defer y.mu.Unlock() 430 431 mf := f.(*memFile) 432 mf.read = false 433 mf.write = true 434 return f, nil 435 } 436 437 // MkdirAll implements FS.MkdirAll. 438 func (y *MemFS) MkdirAll(dirname string, perm os.FileMode) error { 439 return y.walk(dirname, func(dir *memNode, frag string, final bool) error { 440 if frag == "" { 441 if final { 442 return nil 443 } 444 return errors.New("pebble/vfs: empty file name") 445 } 446 child := dir.children[frag] 447 if child == nil { 448 dir.children[frag] = &memNode{ 449 name: frag, 450 children: make(map[string]*memNode), 451 isDir: true, 452 } 453 return nil 454 } 455 if !child.isDir { 456 return &os.PathError{ 457 Op: "open", 458 Path: dirname, 459 Err: errors.New("not a directory"), 460 } 461 } 462 return nil 463 }) 464 } 465 466 // Lock implements FS.Lock. 467 func (y *MemFS) Lock(fullname string) (io.Closer, error) { 468 // FS.Lock excludes other processes, but other processes cannot see this 469 // process' memory. However some uses (eg, Cockroach tests) may open and 470 // close the same MemFS-backed database multiple times. We want mutual 471 // exclusion in this case too. See cockroachdb/cockroach#110645. 472 _, loaded := y.lockedFiles.Swap(fullname, nil /* the value itself is insignificant */) 473 if loaded { 474 // This file lock has already been acquired. On unix, this results in 475 // either EACCES or EAGAIN so we mimic. 476 return nil, syscall.EAGAIN 477 } 478 // Otherwise, we successfully acquired the lock. Locks are visible in the 479 // parent directory listing, and they also must be created under an existent 480 // directory. Create the path so that we have the normal detection of 481 // non-existent directory paths, and make the lock visible when listing 482 // directory entries. 483 f, err := y.Create(fullname) 484 if err != nil { 485 // "Release" the lock since we failed. 486 y.lockedFiles.Delete(fullname) 487 return nil, err 488 } 489 return &memFileLock{ 490 y: y, 491 f: f, 492 fullname: fullname, 493 }, nil 494 } 495 496 // List implements FS.List. 497 func (y *MemFS) List(dirname string) ([]string, error) { 498 if !strings.HasSuffix(dirname, sep) { 499 dirname += sep 500 } 501 var ret []string 502 err := y.walk(dirname, func(dir *memNode, frag string, final bool) error { 503 if final { 504 if frag != "" { 505 panic("unreachable") 506 } 507 ret = make([]string, 0, len(dir.children)) 508 for s := range dir.children { 509 ret = append(ret, s) 510 } 511 } 512 return nil 513 }) 514 return ret, err 515 } 516 517 // Stat implements FS.Stat. 518 func (y *MemFS) Stat(name string) (os.FileInfo, error) { 519 f, err := y.Open(name) 520 if err != nil { 521 if pe, ok := err.(*os.PathError); ok { 522 pe.Op = "stat" 523 } 524 return nil, err 525 } 526 defer f.Close() 527 return f.Stat() 528 } 529 530 // PathBase implements FS.PathBase. 531 func (*MemFS) PathBase(p string) string { 532 // Note that MemFS uses forward slashes for its separator, hence the use of 533 // path.Base, not filepath.Base. 534 return path.Base(p) 535 } 536 537 // PathJoin implements FS.PathJoin. 538 func (*MemFS) PathJoin(elem ...string) string { 539 // Note that MemFS uses forward slashes for its separator, hence the use of 540 // path.Join, not filepath.Join. 541 return path.Join(elem...) 542 } 543 544 // PathDir implements FS.PathDir. 545 func (*MemFS) PathDir(p string) string { 546 // Note that MemFS uses forward slashes for its separator, hence the use of 547 // path.Dir, not filepath.Dir. 548 return path.Dir(p) 549 } 550 551 // GetDiskUsage implements FS.GetDiskUsage. 552 func (*MemFS) GetDiskUsage(string) (DiskUsage, error) { 553 return DiskUsage{}, ErrUnsupported 554 } 555 556 // memNode holds a file's data or a directory's children, and implements os.FileInfo. 557 type memNode struct { 558 name string 559 isDir bool 560 refs atomic.Int32 561 562 // Mutable state. 563 // - For a file: data, syncedDate, modTime: A file is only being mutated by a single goroutine, 564 // but there can be concurrent readers e.g. DB.Checkpoint() which can read WAL or MANIFEST 565 // files that are being written to. Additionally Sync() calls can be concurrent with writing. 566 // - For a directory: children and syncedChildren. Concurrent writes are possible, and 567 // these are protected using MemFS.mu. 568 mu struct { 569 sync.Mutex 570 data []byte 571 syncedData []byte 572 modTime time.Time 573 } 574 575 children map[string]*memNode 576 syncedChildren map[string]*memNode 577 } 578 579 func newRootMemNode() *memNode { 580 return &memNode{ 581 name: "/", // set the name to match what file systems do 582 children: make(map[string]*memNode), 583 isDir: true, 584 } 585 } 586 587 func (f *memNode) IsDir() bool { 588 return f.isDir 589 } 590 591 func (f *memNode) ModTime() time.Time { 592 f.mu.Lock() 593 defer f.mu.Unlock() 594 return f.mu.modTime 595 } 596 597 func (f *memNode) Mode() os.FileMode { 598 if f.isDir { 599 return os.ModeDir | 0755 600 } 601 return 0755 602 } 603 604 func (f *memNode) Name() string { 605 return f.name 606 } 607 608 func (f *memNode) Size() int64 { 609 f.mu.Lock() 610 defer f.mu.Unlock() 611 return int64(len(f.mu.data)) 612 } 613 614 func (f *memNode) Sys() interface{} { 615 return nil 616 } 617 618 func (f *memNode) dump(w *bytes.Buffer, level int) { 619 if f.isDir { 620 w.WriteString(" ") 621 } else { 622 f.mu.Lock() 623 fmt.Fprintf(w, "%8d ", len(f.mu.data)) 624 f.mu.Unlock() 625 } 626 for i := 0; i < level; i++ { 627 w.WriteString(" ") 628 } 629 w.WriteString(f.name) 630 if !f.isDir { 631 w.WriteByte('\n') 632 return 633 } 634 if level > 0 { // deal with the fact that the root's name is already "/" 635 w.WriteByte(sep[0]) 636 } 637 w.WriteByte('\n') 638 names := make([]string, 0, len(f.children)) 639 for name := range f.children { 640 names = append(names, name) 641 } 642 sort.Strings(names) 643 for _, name := range names { 644 f.children[name].dump(w, level+1) 645 } 646 } 647 648 func (f *memNode) resetToSyncedState() { 649 if f.isDir { 650 f.children = make(map[string]*memNode) 651 for k, v := range f.syncedChildren { 652 f.children[k] = v 653 } 654 for _, v := range f.children { 655 v.resetToSyncedState() 656 } 657 } else { 658 f.mu.Lock() 659 f.mu.data = append([]byte(nil), f.mu.syncedData...) 660 f.mu.Unlock() 661 } 662 } 663 664 // memFile is a reader or writer of a node's data, and implements File. 665 type memFile struct { 666 n *memNode 667 fs *MemFS // nil for a standalone memFile 668 rpos int 669 wpos int 670 read, write bool 671 } 672 673 var _ File = (*memFile)(nil) 674 675 func (f *memFile) Close() error { 676 if n := f.n.refs.Add(-1); n < 0 { 677 panic(fmt.Sprintf("pebble: close of unopened file: %d", n)) 678 } 679 f.n = nil 680 return nil 681 } 682 683 func (f *memFile) Read(p []byte) (int, error) { 684 if !f.read { 685 return 0, errors.New("pebble/vfs: file was not opened for reading") 686 } 687 if f.n.isDir { 688 return 0, errors.New("pebble/vfs: cannot read a directory") 689 } 690 f.n.mu.Lock() 691 defer f.n.mu.Unlock() 692 if f.rpos >= len(f.n.mu.data) { 693 return 0, io.EOF 694 } 695 n := copy(p, f.n.mu.data[f.rpos:]) 696 f.rpos += n 697 return n, nil 698 } 699 700 func (f *memFile) ReadAt(p []byte, off int64) (int, error) { 701 if !f.read { 702 return 0, errors.New("pebble/vfs: file was not opened for reading") 703 } 704 if f.n.isDir { 705 return 0, errors.New("pebble/vfs: cannot read a directory") 706 } 707 f.n.mu.Lock() 708 defer f.n.mu.Unlock() 709 if off >= int64(len(f.n.mu.data)) { 710 return 0, io.EOF 711 } 712 n := copy(p, f.n.mu.data[off:]) 713 if n < len(p) { 714 return n, io.EOF 715 } 716 return n, nil 717 } 718 719 func (f *memFile) Write(p []byte) (int, error) { 720 if !f.write { 721 return 0, errors.New("pebble/vfs: file was not created for writing") 722 } 723 if f.n.isDir { 724 return 0, errors.New("pebble/vfs: cannot write a directory") 725 } 726 f.n.mu.Lock() 727 defer f.n.mu.Unlock() 728 f.n.mu.modTime = time.Now() 729 if f.wpos+len(p) <= len(f.n.mu.data) { 730 n := copy(f.n.mu.data[f.wpos:f.wpos+len(p)], p) 731 if n != len(p) { 732 panic("stuff") 733 } 734 } else { 735 f.n.mu.data = append(f.n.mu.data[:f.wpos], p...) 736 } 737 f.wpos += len(p) 738 739 if invariants.Enabled { 740 // Mutate the input buffer to flush out bugs in Pebble which expect the 741 // input buffer to be unmodified. 742 for i := range p { 743 p[i] ^= 0xff 744 } 745 } 746 return len(p), nil 747 } 748 749 func (f *memFile) WriteAt(p []byte, ofs int64) (int, error) { 750 if !f.write { 751 return 0, errors.New("pebble/vfs: file was not created for writing") 752 } 753 if f.n.isDir { 754 return 0, errors.New("pebble/vfs: cannot write a directory") 755 } 756 f.n.mu.Lock() 757 defer f.n.mu.Unlock() 758 f.n.mu.modTime = time.Now() 759 760 for len(f.n.mu.data) < int(ofs)+len(p) { 761 f.n.mu.data = append(f.n.mu.data, 0) 762 } 763 764 n := copy(f.n.mu.data[int(ofs):int(ofs)+len(p)], p) 765 if n != len(p) { 766 panic("stuff") 767 } 768 769 return len(p), nil 770 } 771 772 func (f *memFile) Prefetch(offset int64, length int64) error { return nil } 773 func (f *memFile) Preallocate(offset, length int64) error { return nil } 774 775 func (f *memFile) Stat() (os.FileInfo, error) { 776 return f.n, nil 777 } 778 779 func (f *memFile) Sync() error { 780 if f.fs != nil && f.fs.strict { 781 f.fs.mu.Lock() 782 defer f.fs.mu.Unlock() 783 if f.fs.ignoreSyncs { 784 return nil 785 } 786 if f.n.isDir { 787 f.n.syncedChildren = make(map[string]*memNode) 788 for k, v := range f.n.children { 789 f.n.syncedChildren[k] = v 790 } 791 } else { 792 f.n.mu.Lock() 793 f.n.mu.syncedData = append([]byte(nil), f.n.mu.data...) 794 f.n.mu.Unlock() 795 } 796 } 797 return nil 798 } 799 800 func (f *memFile) SyncData() error { 801 return f.Sync() 802 } 803 804 func (f *memFile) SyncTo(length int64) (fullSync bool, err error) { 805 // NB: This SyncTo implementation lies, with its return values claiming it 806 // synced the data up to `length`. When fullSync=false, SyncTo provides no 807 // durability guarantees, so this can help surface bugs where we improperly 808 // rely on SyncTo providing durability. 809 return false, nil 810 } 811 812 func (f *memFile) Fd() uintptr { 813 return InvalidFd 814 } 815 816 // Flush is a no-op and present only to prevent buffering at higher levels 817 // (e.g. it prevents sstable.Writer from using a bufio.Writer). 818 func (f *memFile) Flush() error { 819 return nil 820 } 821 822 type memFileLock struct { 823 y *MemFS 824 f File 825 fullname string 826 } 827 828 func (l *memFileLock) Close() error { 829 if l.y == nil { 830 return nil 831 } 832 l.y.lockedFiles.Delete(l.fullname) 833 l.y = nil 834 return l.f.Close() 835 }