syncthing-arm/lib/fs/mtimefs.go
Jakob Borg c71116ee94
Implement database abstraction, error checking (ref #5907) (#6107)
This PR does two things, because one lead to the other:

- Move the leveldb specific stuff into a small "backend" package that
defines a backend interface and the leveldb implementation. This allows,
potentially, in the future, switching the db implementation so another
KV store should we wish to do so.

- Add proper error handling all along the way. The db and backend
packages are now errcheck clean. However, I drew the line at modifying
the FileSet API in order to keep this manageable and not continue
refactoring all of the rest of Syncthing. As such, the FileSet methods
still panic on database errors, except for the "database is closed"
error which is instead handled by silently returning as quickly as
possible, with the assumption that we're anyway "on the way out".
2019-11-29 09:11:52 +01:00

238 lines
4.9 KiB
Go

// Copyright (C) 2016 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
package fs
import (
"time"
)
// The database is where we store the virtual mtimes
type database interface {
Bytes(key string) (data []byte, ok bool)
PutBytes(key string, data []byte) error
Delete(key string) error
}
// The MtimeFS is a filesystem with nanosecond mtime precision, regardless
// of what shenanigans the underlying filesystem gets up to. A nil MtimeFS
// just does the underlying operations with no additions.
type MtimeFS struct {
Filesystem
chtimes func(string, time.Time, time.Time) error
db database
caseInsensitive bool
}
type MtimeFSOption func(*MtimeFS)
func WithCaseInsensitivity(v bool) MtimeFSOption {
return func(f *MtimeFS) {
f.caseInsensitive = v
}
}
func NewMtimeFS(underlying Filesystem, db database, options ...MtimeFSOption) *MtimeFS {
f := &MtimeFS{
Filesystem: underlying,
chtimes: underlying.Chtimes, // for mocking it out in the tests
db: db,
}
for _, opt := range options {
opt(f)
}
return f
}
func (f *MtimeFS) Chtimes(name string, atime, mtime time.Time) error {
if f == nil {
return f.chtimes(name, atime, mtime)
}
// Do a normal Chtimes call, don't care if it succeeds or not.
f.chtimes(name, atime, mtime)
// Stat the file to see what happened. Here we *do* return an error,
// because it might be "does not exist" or similar.
info, err := f.Filesystem.Lstat(name)
if err != nil {
return err
}
f.save(name, info.ModTime(), mtime)
return nil
}
func (f *MtimeFS) Stat(name string) (FileInfo, error) {
info, err := f.Filesystem.Stat(name)
if err != nil {
return nil, err
}
real, virtual := f.load(name)
if real == info.ModTime() {
info = mtimeFileInfo{
FileInfo: info,
mtime: virtual,
}
}
return info, nil
}
func (f *MtimeFS) Lstat(name string) (FileInfo, error) {
info, err := f.Filesystem.Lstat(name)
if err != nil {
return nil, err
}
real, virtual := f.load(name)
if real == info.ModTime() {
info = mtimeFileInfo{
FileInfo: info,
mtime: virtual,
}
}
return info, nil
}
func (f *MtimeFS) Walk(root string, walkFn WalkFunc) error {
return f.Filesystem.Walk(root, func(path string, info FileInfo, err error) error {
if info != nil {
real, virtual := f.load(path)
if real == info.ModTime() {
info = mtimeFileInfo{
FileInfo: info,
mtime: virtual,
}
}
}
return walkFn(path, info, err)
})
}
func (f *MtimeFS) Create(name string) (File, error) {
fd, err := f.Filesystem.Create(name)
if err != nil {
return nil, err
}
return &mtimeFile{fd, f}, nil
}
func (f *MtimeFS) Open(name string) (File, error) {
fd, err := f.Filesystem.Open(name)
if err != nil {
return nil, err
}
return &mtimeFile{fd, f}, nil
}
func (f *MtimeFS) OpenFile(name string, flags int, mode FileMode) (File, error) {
fd, err := f.Filesystem.OpenFile(name, flags, mode)
if err != nil {
return nil, err
}
return &mtimeFile{fd, f}, nil
}
// "real" is the on disk timestamp
// "virtual" is what want the timestamp to be
func (f *MtimeFS) save(name string, real, virtual time.Time) {
if f.caseInsensitive {
name = UnicodeLowercase(name)
}
if real.Equal(virtual) {
// If the virtual time and the real on disk time are equal we don't
// need to store anything.
f.db.Delete(name)
return
}
mtime := dbMtime{
real: real,
virtual: virtual,
}
bs, _ := mtime.Marshal() // Can't fail
f.db.PutBytes(name, bs)
}
func (f *MtimeFS) load(name string) (real, virtual time.Time) {
if f.caseInsensitive {
name = UnicodeLowercase(name)
}
data, exists := f.db.Bytes(name)
if !exists {
return
}
var mtime dbMtime
if err := mtime.Unmarshal(data); err != nil {
return
}
return mtime.real, mtime.virtual
}
// The mtimeFileInfo is an os.FileInfo that lies about the ModTime().
type mtimeFileInfo struct {
FileInfo
mtime time.Time
}
func (m mtimeFileInfo) ModTime() time.Time {
return m.mtime
}
type mtimeFile struct {
File
fs *MtimeFS
}
func (f *mtimeFile) Stat() (FileInfo, error) {
info, err := f.File.Stat()
if err != nil {
return nil, err
}
real, virtual := f.fs.load(f.Name())
if real == info.ModTime() {
info = mtimeFileInfo{
FileInfo: info,
mtime: virtual,
}
}
return info, nil
}
// The dbMtime is our database representation
type dbMtime struct {
real time.Time
virtual time.Time
}
func (t *dbMtime) Marshal() ([]byte, error) {
bs0, _ := t.real.MarshalBinary()
bs1, _ := t.virtual.MarshalBinary()
return append(bs0, bs1...), nil
}
func (t *dbMtime) Unmarshal(bs []byte) error {
if err := t.real.UnmarshalBinary(bs[:len(bs)/2]); err != nil {
return err
}
if err := t.virtual.UnmarshalBinary(bs[len(bs)/2:]); err != nil {
return err
}
return nil
}