2015-03-28 10:50:23 +00:00
|
|
|
package local
|
|
|
|
|
|
|
|
import (
|
2017-06-03 15:39:57 +00:00
|
|
|
"context"
|
2024-05-10 22:07:04 +00:00
|
|
|
"fmt"
|
2020-12-19 11:39:48 +00:00
|
|
|
"hash"
|
2015-03-28 10:50:23 +00:00
|
|
|
"io"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
2019-10-09 19:42:15 +00:00
|
|
|
"syscall"
|
2015-03-28 10:50:23 +00:00
|
|
|
|
2017-07-23 12:21:03 +00:00
|
|
|
"github.com/restic/restic/internal/backend"
|
2022-10-15 14:23:39 +00:00
|
|
|
"github.com/restic/restic/internal/backend/layout"
|
2023-06-08 11:04:34 +00:00
|
|
|
"github.com/restic/restic/internal/backend/limiter"
|
|
|
|
"github.com/restic/restic/internal/backend/location"
|
2023-10-01 08:24:33 +00:00
|
|
|
"github.com/restic/restic/internal/backend/util"
|
2017-07-23 12:21:03 +00:00
|
|
|
"github.com/restic/restic/internal/debug"
|
2022-06-12 15:45:34 +00:00
|
|
|
"github.com/restic/restic/internal/errors"
|
2017-07-23 12:21:03 +00:00
|
|
|
"github.com/restic/restic/internal/fs"
|
2020-12-16 12:58:02 +00:00
|
|
|
|
|
|
|
"github.com/cenkalti/backoff/v4"
|
2015-03-28 10:50:23 +00:00
|
|
|
)
|
|
|
|
|
2016-01-24 19:23:50 +00:00
|
|
|
// Local is a backend in a local directory.
|
2015-03-28 10:50:23 +00:00
|
|
|
type Local struct {
|
2017-03-25 12:20:03 +00:00
|
|
|
Config
|
2022-10-15 14:23:39 +00:00
|
|
|
layout.Layout
|
2023-10-01 08:24:33 +00:00
|
|
|
util.Modes
|
2015-03-28 10:50:23 +00:00
|
|
|
}
|
|
|
|
|
2023-10-01 09:40:12 +00:00
|
|
|
// ensure statically that *Local implements backend.Backend.
|
|
|
|
var _ backend.Backend = &Local{}
|
2016-08-31 20:39:36 +00:00
|
|
|
|
2024-05-10 22:07:04 +00:00
|
|
|
var errTooShort = fmt.Errorf("file is too short")
|
|
|
|
|
2023-06-08 11:04:34 +00:00
|
|
|
func NewFactory() location.Factory {
|
2023-06-08 15:32:43 +00:00
|
|
|
return location.NewLimitedBackendFactory("local", ParseConfig, location.NoPassword, limiter.WrapBackendConstructor(Create), limiter.WrapBackendConstructor(Open))
|
2023-06-08 11:04:34 +00:00
|
|
|
}
|
|
|
|
|
2024-08-26 19:16:22 +00:00
|
|
|
func open(cfg Config) (*Local, error) {
|
2024-08-26 18:28:39 +00:00
|
|
|
l := layout.NewDefaultLayout(cfg.Path, filepath.Join)
|
2017-03-26 19:53:26 +00:00
|
|
|
|
2024-07-21 13:22:21 +00:00
|
|
|
fi, err := os.Stat(l.Filename(backend.Handle{Type: backend.ConfigFile}))
|
2023-10-01 08:24:33 +00:00
|
|
|
m := util.DeriveModesFromFileInfo(fi, err)
|
2022-04-26 17:15:09 +00:00
|
|
|
debug.Log("using (%03O file, %03O dir) permissions", m.File, m.Dir)
|
|
|
|
|
2021-08-07 17:50:00 +00:00
|
|
|
return &Local{
|
|
|
|
Config: cfg,
|
|
|
|
Layout: l,
|
2022-04-26 17:15:09 +00:00
|
|
|
Modes: m,
|
2021-08-07 17:50:00 +00:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Open opens the local backend as specified by config.
|
2024-08-26 19:16:22 +00:00
|
|
|
func Open(_ context.Context, cfg Config) (*Local, error) {
|
2024-08-26 18:28:39 +00:00
|
|
|
debug.Log("open local backend at %v", cfg.Path)
|
2024-08-26 19:16:22 +00:00
|
|
|
return open(cfg)
|
2015-03-28 10:50:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Create creates all the necessary files and directories for a new local
|
2015-05-04 18:39:45 +00:00
|
|
|
// backend at dir. Afterwards a new config blob should be created.
|
2024-08-26 19:16:22 +00:00
|
|
|
func Create(_ context.Context, cfg Config) (*Local, error) {
|
2024-08-26 18:28:39 +00:00
|
|
|
debug.Log("create local backend at %v", cfg.Path)
|
2017-04-02 17:54:11 +00:00
|
|
|
|
2024-08-26 19:16:22 +00:00
|
|
|
be, err := open(cfg)
|
2017-04-02 17:54:11 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2015-05-04 18:39:45 +00:00
|
|
|
// test if config file already exists
|
2024-07-21 13:22:21 +00:00
|
|
|
_, err = os.Lstat(be.Filename(backend.Handle{Type: backend.ConfigFile}))
|
2015-03-28 10:50:23 +00:00
|
|
|
if err == nil {
|
2015-05-03 14:43:27 +00:00
|
|
|
return nil, errors.New("config file already exists")
|
2015-03-28 10:50:23 +00:00
|
|
|
}
|
|
|
|
|
2017-04-19 16:56:01 +00:00
|
|
|
// create paths for data and refs
|
2017-03-26 19:53:26 +00:00
|
|
|
for _, d := range be.Paths() {
|
2024-07-21 13:22:21 +00:00
|
|
|
err := os.MkdirAll(d, be.Modes.Dir)
|
2015-03-28 10:50:23 +00:00
|
|
|
if err != nil {
|
2021-06-07 17:26:25 +00:00
|
|
|
return nil, errors.WithStack(err)
|
2015-03-28 10:50:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-03-26 19:53:26 +00:00
|
|
|
return be, nil
|
2015-03-28 10:50:23 +00:00
|
|
|
}
|
|
|
|
|
2021-08-07 17:50:00 +00:00
|
|
|
func (b *Local) Connections() uint {
|
|
|
|
return b.Config.Connections
|
|
|
|
}
|
|
|
|
|
2020-12-19 11:39:48 +00:00
|
|
|
// Hasher may return a hash function for calculating a content hash for the backend
|
|
|
|
func (b *Local) Hasher() hash.Hash {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-05-01 18:07:29 +00:00
|
|
|
// HasAtomicReplace returns whether Save() can atomically replace files
|
|
|
|
func (b *Local) HasAtomicReplace() bool {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2017-06-15 11:40:27 +00:00
|
|
|
// IsNotExist returns true if the error is caused by a non existing file.
|
|
|
|
func (b *Local) IsNotExist(err error) bool {
|
2021-06-07 17:26:25 +00:00
|
|
|
return errors.Is(err, os.ErrNotExist)
|
2017-06-15 11:40:27 +00:00
|
|
|
}
|
|
|
|
|
2024-05-10 22:07:04 +00:00
|
|
|
func (b *Local) IsPermanentError(err error) bool {
|
|
|
|
return b.IsNotExist(err) || errors.Is(err, errTooShort) || errors.Is(err, os.ErrPermission)
|
|
|
|
}
|
|
|
|
|
2016-01-26 21:12:53 +00:00
|
|
|
// Save stores data in the backend at the handle.
|
2023-10-01 09:40:12 +00:00
|
|
|
func (b *Local) Save(_ context.Context, h backend.Handle, rd backend.RewindReader) (err error) {
|
2020-11-02 09:34:21 +00:00
|
|
|
finalname := b.Filename(h)
|
|
|
|
dir := filepath.Dir(finalname)
|
2016-01-24 15:59:38 +00:00
|
|
|
|
2020-12-16 12:58:02 +00:00
|
|
|
defer func() {
|
2020-12-20 20:12:27 +00:00
|
|
|
// Mark non-retriable errors as such
|
|
|
|
if errors.Is(err, syscall.ENOSPC) || os.IsPermission(err) {
|
2020-12-16 12:58:02 +00:00
|
|
|
err = backoff.Permanent(err)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
2020-11-02 09:34:21 +00:00
|
|
|
// Create new file with a temporary name.
|
|
|
|
tmpname := filepath.Base(finalname) + "-tmp-"
|
|
|
|
f, err := tempFile(dir, tmpname)
|
2018-01-05 16:51:09 +00:00
|
|
|
|
|
|
|
if b.IsNotExist(err) {
|
|
|
|
debug.Log("error %v: creating dir", err)
|
|
|
|
|
|
|
|
// error is caused by a missing directory, try to create it
|
2024-07-21 13:22:21 +00:00
|
|
|
mkdirErr := os.MkdirAll(dir, b.Modes.Dir)
|
2018-01-05 16:51:09 +00:00
|
|
|
if mkdirErr != nil {
|
2020-11-02 09:34:21 +00:00
|
|
|
debug.Log("error creating dir %v: %v", dir, mkdirErr)
|
2018-01-05 16:51:09 +00:00
|
|
|
} else {
|
|
|
|
// try again
|
2020-11-02 09:34:21 +00:00
|
|
|
f, err = tempFile(dir, tmpname)
|
2018-01-05 16:51:09 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-03-26 19:53:26 +00:00
|
|
|
if err != nil {
|
2021-06-07 17:26:25 +00:00
|
|
|
return errors.WithStack(err)
|
2017-03-26 19:53:26 +00:00
|
|
|
}
|
|
|
|
|
2020-11-02 09:34:21 +00:00
|
|
|
defer func(f *os.File) {
|
|
|
|
if err != nil {
|
|
|
|
_ = f.Close() // Double Close is harmless.
|
|
|
|
// Remove after Rename is harmless: we embed the final name in the
|
|
|
|
// temporary's name and no other goroutine will get the same data to
|
|
|
|
// Save, so the temporary name should never be reused by another
|
|
|
|
// goroutine.
|
2024-07-21 13:22:21 +00:00
|
|
|
_ = os.Remove(f.Name())
|
2020-11-02 09:34:21 +00:00
|
|
|
}
|
|
|
|
}(f)
|
|
|
|
|
2021-02-02 14:44:40 +00:00
|
|
|
// preallocate disk space
|
|
|
|
if size := rd.Length(); size > 0 {
|
|
|
|
if err := fs.PreallocateFile(f, size); err != nil {
|
|
|
|
debug.Log("Failed to preallocate %v with size %v: %v", finalname, size, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-03-26 19:53:26 +00:00
|
|
|
// save data, then sync
|
2020-12-18 22:41:29 +00:00
|
|
|
wbytes, err := io.Copy(f, rd)
|
2017-03-26 19:53:26 +00:00
|
|
|
if err != nil {
|
2021-06-07 17:26:25 +00:00
|
|
|
return errors.WithStack(err)
|
2017-03-26 19:53:26 +00:00
|
|
|
}
|
2020-12-18 22:41:29 +00:00
|
|
|
// sanity check
|
|
|
|
if wbytes != rd.Length() {
|
|
|
|
return errors.Errorf("wrote %d bytes instead of the expected %d bytes", wbytes, rd.Length())
|
|
|
|
}
|
2017-03-26 19:53:26 +00:00
|
|
|
|
2020-11-02 09:34:21 +00:00
|
|
|
// Ignore error if filesystem does not support fsync.
|
2021-07-09 15:11:39 +00:00
|
|
|
err = f.Sync()
|
2022-11-11 13:53:42 +00:00
|
|
|
syncNotSup := err != nil && (errors.Is(err, syscall.ENOTSUP) || isMacENOTTY(err))
|
2021-07-09 15:11:39 +00:00
|
|
|
if err != nil && !syncNotSup {
|
2020-11-02 09:34:21 +00:00
|
|
|
return errors.WithStack(err)
|
2017-03-26 19:53:26 +00:00
|
|
|
}
|
2016-01-24 15:59:38 +00:00
|
|
|
|
2020-11-02 09:34:21 +00:00
|
|
|
// Close, then rename. Windows doesn't like the reverse order.
|
|
|
|
if err = f.Close(); err != nil {
|
|
|
|
return errors.WithStack(err)
|
|
|
|
}
|
|
|
|
if err = os.Rename(f.Name(), finalname); err != nil {
|
2021-06-07 17:26:25 +00:00
|
|
|
return errors.WithStack(err)
|
2016-01-24 15:59:38 +00:00
|
|
|
}
|
|
|
|
|
2021-07-09 15:11:39 +00:00
|
|
|
// Now sync the directory to commit the Rename.
|
|
|
|
if !syncNotSup {
|
|
|
|
err = fsyncDir(dir)
|
|
|
|
if err != nil {
|
|
|
|
return errors.WithStack(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-06 12:11:55 +00:00
|
|
|
// try to mark file as read-only to avoid accidental modifications
|
2020-10-06 16:28:01 +00:00
|
|
|
// ignore if the operation fails as some filesystems don't allow the chmod call
|
|
|
|
// e.g. exfat and network file systems with certain mount options
|
2022-04-26 17:15:09 +00:00
|
|
|
err = setFileReadonly(finalname, b.Modes.File)
|
2020-10-06 16:28:01 +00:00
|
|
|
if err != nil && !os.IsPermission(err) {
|
2021-06-07 17:26:25 +00:00
|
|
|
return errors.WithStack(err)
|
2020-10-06 16:28:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
2016-01-24 00:15:35 +00:00
|
|
|
}
|
|
|
|
|
2022-12-02 18:36:43 +00:00
|
|
|
var tempFile = os.CreateTemp // Overridden by test.
|
2020-12-16 12:58:02 +00:00
|
|
|
|
2018-01-17 04:59:16 +00:00
|
|
|
// Load runs fn with a reader that yields the contents of the file at h at the
|
|
|
|
// given offset.
|
2023-10-01 09:40:12 +00:00
|
|
|
func (b *Local) Load(ctx context.Context, h backend.Handle, length int, offset int64, fn func(rd io.Reader) error) error {
|
2023-10-01 08:24:33 +00:00
|
|
|
return util.DefaultLoad(ctx, h, length, offset, b.openReader, fn)
|
2018-01-17 04:59:16 +00:00
|
|
|
}
|
|
|
|
|
2023-10-01 09:40:12 +00:00
|
|
|
func (b *Local) openReader(_ context.Context, h backend.Handle, length int, offset int64) (io.ReadCloser, error) {
|
2024-07-21 13:22:21 +00:00
|
|
|
f, err := os.Open(b.Filename(h))
|
2017-01-22 21:01:12 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2024-05-10 22:07:04 +00:00
|
|
|
fi, err := f.Stat()
|
|
|
|
if err != nil {
|
|
|
|
_ = f.Close()
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
size := fi.Size()
|
|
|
|
if size < offset+int64(length) {
|
|
|
|
_ = f.Close()
|
|
|
|
return nil, errTooShort
|
|
|
|
}
|
|
|
|
|
2017-01-22 21:01:12 +00:00
|
|
|
if offset > 0 {
|
|
|
|
_, err = f.Seek(offset, 0)
|
|
|
|
if err != nil {
|
2017-06-03 15:39:57 +00:00
|
|
|
_ = f.Close()
|
2017-01-22 21:01:12 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if length > 0 {
|
2024-04-25 19:20:23 +00:00
|
|
|
return util.LimitReadCloser(f, int64(length)), nil
|
2017-01-22 21:01:12 +00:00
|
|
|
}
|
|
|
|
|
2023-04-07 21:02:35 +00:00
|
|
|
return f, nil
|
2017-01-22 21:01:12 +00:00
|
|
|
}
|
|
|
|
|
2016-01-23 22:27:58 +00:00
|
|
|
// Stat returns information about a blob.
|
2023-10-01 09:40:12 +00:00
|
|
|
func (b *Local) Stat(_ context.Context, h backend.Handle) (backend.FileInfo, error) {
|
2024-07-21 13:22:21 +00:00
|
|
|
fi, err := os.Stat(b.Filename(h))
|
2016-01-23 22:27:58 +00:00
|
|
|
if err != nil {
|
2023-10-01 09:40:12 +00:00
|
|
|
return backend.FileInfo{}, errors.WithStack(err)
|
2016-01-23 22:27:58 +00:00
|
|
|
}
|
|
|
|
|
2023-10-01 09:40:12 +00:00
|
|
|
return backend.FileInfo{Size: fi.Size(), Name: h.Name}, nil
|
2016-01-23 22:27:58 +00:00
|
|
|
}
|
|
|
|
|
2015-03-28 10:50:23 +00:00
|
|
|
// Remove removes the blob with the given name and type.
|
2023-10-01 09:40:12 +00:00
|
|
|
func (b *Local) Remove(_ context.Context, h backend.Handle) error {
|
2017-03-26 19:53:26 +00:00
|
|
|
fn := b.Filename(h)
|
2015-08-14 13:30:36 +00:00
|
|
|
|
2015-08-19 20:02:47 +00:00
|
|
|
// reset read-only flag
|
2024-07-21 13:22:21 +00:00
|
|
|
err := os.Chmod(fn, 0666)
|
2020-10-06 16:28:01 +00:00
|
|
|
if err != nil && !os.IsPermission(err) {
|
2021-06-07 17:26:25 +00:00
|
|
|
return errors.WithStack(err)
|
2015-08-19 20:02:47 +00:00
|
|
|
}
|
|
|
|
|
2024-07-21 13:22:21 +00:00
|
|
|
return os.Remove(fn)
|
2015-03-28 10:50:23 +00:00
|
|
|
}
|
|
|
|
|
2018-01-20 18:34:38 +00:00
|
|
|
// List runs fn for each file in the backend which has the type t. When an
|
|
|
|
// error occurs (or fn returns an error), List stops and returns it.
|
2023-10-01 09:40:12 +00:00
|
|
|
func (b *Local) List(ctx context.Context, t backend.FileType, fn func(backend.FileInfo) error) (err error) {
|
2018-01-20 12:43:07 +00:00
|
|
|
basedir, subdirs := b.Basedir(t)
|
2020-11-19 15:46:42 +00:00
|
|
|
if subdirs {
|
|
|
|
err = visitDirs(ctx, basedir, fn)
|
|
|
|
} else {
|
2021-02-26 23:02:13 +00:00
|
|
|
err = visitFiles(ctx, basedir, fn, false)
|
2020-11-19 15:46:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if b.IsNotExist(err) {
|
|
|
|
debug.Log("ignoring non-existing directory")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// The following two functions are like filepath.Walk, but visit only one or
|
|
|
|
// two levels of directory structure (including dir itself as the first level).
|
|
|
|
// Also, visitDirs assumes it sees a directory full of directories, while
|
|
|
|
// visitFiles wants a directory full or regular files.
|
2023-10-01 09:40:12 +00:00
|
|
|
func visitDirs(ctx context.Context, dir string, fn func(backend.FileInfo) error) error {
|
2024-07-21 13:22:21 +00:00
|
|
|
d, err := os.Open(dir)
|
2020-11-19 15:46:42 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
sub, err := d.Readdirnames(-1)
|
2021-01-30 18:35:46 +00:00
|
|
|
if err != nil {
|
|
|
|
// ignore subsequent errors
|
|
|
|
_ = d.Close()
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
err = d.Close()
|
2020-11-19 15:46:42 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, f := range sub {
|
2021-02-26 23:02:13 +00:00
|
|
|
err = visitFiles(ctx, filepath.Join(dir, f), fn, true)
|
2018-01-20 12:43:07 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-11-19 15:46:42 +00:00
|
|
|
}
|
|
|
|
return ctx.Err()
|
|
|
|
}
|
2015-03-28 10:50:23 +00:00
|
|
|
|
2023-10-01 09:40:12 +00:00
|
|
|
func visitFiles(ctx context.Context, dir string, fn func(backend.FileInfo) error, ignoreNotADirectory bool) error {
|
2024-07-21 13:22:21 +00:00
|
|
|
d, err := os.Open(dir)
|
2020-11-19 15:46:42 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2015-03-28 10:50:23 +00:00
|
|
|
|
2021-02-26 23:02:13 +00:00
|
|
|
if ignoreNotADirectory {
|
|
|
|
fi, err := d.Stat()
|
|
|
|
if err != nil || !fi.IsDir() {
|
2021-11-06 18:44:57 +00:00
|
|
|
// ignore subsequent errors
|
|
|
|
_ = d.Close()
|
2021-02-26 23:02:13 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-19 15:46:42 +00:00
|
|
|
sub, err := d.Readdir(-1)
|
2021-01-30 18:35:46 +00:00
|
|
|
if err != nil {
|
|
|
|
// ignore subsequent errors
|
|
|
|
_ = d.Close()
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
err = d.Close()
|
2020-11-19 15:46:42 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2017-12-14 18:13:01 +00:00
|
|
|
|
2020-11-19 15:46:42 +00:00
|
|
|
for _, fi := range sub {
|
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
2018-01-20 18:34:38 +00:00
|
|
|
return ctx.Err()
|
2020-11-19 15:46:42 +00:00
|
|
|
default:
|
2018-01-20 18:34:38 +00:00
|
|
|
}
|
|
|
|
|
2023-10-01 09:40:12 +00:00
|
|
|
err := fn(backend.FileInfo{
|
2020-11-19 15:46:42 +00:00
|
|
|
Name: fi.Name(),
|
|
|
|
Size: fi.Size(),
|
|
|
|
})
|
2018-01-20 12:43:07 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2018-04-10 19:35:30 +00:00
|
|
|
}
|
2020-11-19 15:46:42 +00:00
|
|
|
return nil
|
2015-03-28 10:50:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Delete removes the repository and all files.
|
2023-05-18 17:18:09 +00:00
|
|
|
func (b *Local) Delete(_ context.Context) error {
|
2024-07-21 13:22:21 +00:00
|
|
|
return os.RemoveAll(b.Path)
|
2015-08-14 13:30:36 +00:00
|
|
|
}
|
2015-03-28 10:50:23 +00:00
|
|
|
|
2015-08-14 13:30:36 +00:00
|
|
|
// Close closes all open files.
|
|
|
|
func (b *Local) Close() error {
|
2016-01-26 21:07:51 +00:00
|
|
|
// this does not need to do anything, all open files are closed within the
|
|
|
|
// same function.
|
2015-08-14 13:30:36 +00:00
|
|
|
return nil
|
|
|
|
}
|