mirror of
https://github.com/octoleo/syncthing.git
synced 2024-11-10 15:20:56 +00:00
adce6fa473
This adds support for syncing ownership on Unixes and on Windows. The scanner always picks up ownership information, but it is not applied unless the new folder option "Sync Ownership" is set. Ownership data is stored in a new FileInfo field called "platform data". This is intended to hold further platform-specific data in the future (specifically, extended attributes), which is why the whole design is a bit overkill for just ownership.
268 lines
5.6 KiB
Go
268 lines
5.6 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 (
|
|
"errors"
|
|
"time"
|
|
)
|
|
|
|
// The database is where we store the virtual mtimes
|
|
type database interface {
|
|
Bytes(key string) (data []byte, ok bool, err error)
|
|
PutBytes(key string, data []byte) error
|
|
Delete(key string) error
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
type optionMtime struct {
|
|
db database
|
|
options []MtimeFSOption
|
|
}
|
|
|
|
// NewMtimeOption makes any filesystem provide nanosecond mtime precision,
|
|
// regardless of what shenanigans the underlying filesystem gets up to.
|
|
func NewMtimeOption(db database, options ...MtimeFSOption) Option {
|
|
return &optionMtime{
|
|
db: db,
|
|
options: options,
|
|
}
|
|
}
|
|
|
|
func (o *optionMtime) apply(fs Filesystem) Filesystem {
|
|
f := &mtimeFS{
|
|
Filesystem: fs,
|
|
chtimes: fs.Chtimes, // for mocking it out in the tests
|
|
db: o.db,
|
|
}
|
|
for _, opt := range o.options {
|
|
opt(f)
|
|
}
|
|
return f
|
|
}
|
|
|
|
func (*optionMtime) String() string {
|
|
return "mtime"
|
|
}
|
|
|
|
func (f *mtimeFS) Chtimes(name string, atime, mtime time.Time) error {
|
|
// 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
|
|
}
|
|
|
|
mtimeMapping, err := f.load(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if mtimeMapping.Real == info.ModTime() {
|
|
info = mtimeFileInfo{
|
|
FileInfo: info,
|
|
mtime: mtimeMapping.Virtual,
|
|
}
|
|
}
|
|
|
|
return info, nil
|
|
}
|
|
|
|
func (f *mtimeFS) Lstat(name string) (FileInfo, error) {
|
|
info, err := f.Filesystem.Lstat(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
mtimeMapping, err := f.load(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if mtimeMapping.Real == info.ModTime() {
|
|
info = mtimeFileInfo{
|
|
FileInfo: info,
|
|
mtime: mtimeMapping.Virtual,
|
|
}
|
|
}
|
|
|
|
return info, nil
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func (f *mtimeFS) underlying() (Filesystem, bool) {
|
|
return f.Filesystem, true
|
|
}
|
|
|
|
func (f *mtimeFS) wrapperType() filesystemWrapperType {
|
|
return filesystemWrapperTypeMtime
|
|
}
|
|
|
|
func (f *mtimeFS) save(name string, real, virtual time.Time) {
|
|
if f.caseInsensitive {
|
|
name = UnicodeLowercaseNormalized(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 := MtimeMapping{
|
|
Real: real,
|
|
Virtual: virtual,
|
|
}
|
|
bs, _ := mtime.Marshal() // Can't fail
|
|
f.db.PutBytes(name, bs)
|
|
}
|
|
|
|
func (f *mtimeFS) load(name string) (MtimeMapping, error) {
|
|
if f.caseInsensitive {
|
|
name = UnicodeLowercaseNormalized(name)
|
|
}
|
|
|
|
data, exists, err := f.db.Bytes(name)
|
|
if err != nil {
|
|
return MtimeMapping{}, err
|
|
} else if !exists {
|
|
return MtimeMapping{}, nil
|
|
}
|
|
|
|
var mtime MtimeMapping
|
|
if err := mtime.Unmarshal(data); err != nil {
|
|
return MtimeMapping{}, err
|
|
}
|
|
return mtime, nil
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
mtimeMapping, err := f.fs.load(f.Name())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if mtimeMapping.Real == info.ModTime() {
|
|
info = mtimeFileInfo{
|
|
FileInfo: info,
|
|
mtime: mtimeMapping.Virtual,
|
|
}
|
|
}
|
|
|
|
return info, nil
|
|
}
|
|
|
|
// Used by copyRange to unwrap to the real file and access SyscallConn
|
|
func (f mtimeFile) unwrap() File {
|
|
return f.File
|
|
}
|
|
|
|
// MtimeMapping represents the mapping as stored in the database
|
|
type MtimeMapping struct {
|
|
// "Real" is the on disk timestamp
|
|
Real time.Time `json:"real"`
|
|
// "Virtual" is what want the timestamp to be
|
|
Virtual time.Time `json:"virtual"`
|
|
}
|
|
|
|
func (t *MtimeMapping) Marshal() ([]byte, error) {
|
|
bs0, _ := t.Real.MarshalBinary()
|
|
bs1, _ := t.Virtual.MarshalBinary()
|
|
return append(bs0, bs1...), nil
|
|
}
|
|
|
|
func (t *MtimeMapping) 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
|
|
}
|
|
|
|
func GetMtimeMapping(fs Filesystem, file string) (MtimeMapping, error) {
|
|
fs, ok := unwrapFilesystem(fs, filesystemWrapperTypeMtime)
|
|
if !ok {
|
|
return MtimeMapping{}, errors.New("failed to unwrap")
|
|
}
|
|
mtimeFs, ok := fs.(*mtimeFS)
|
|
if !ok {
|
|
return MtimeMapping{}, errors.New("unwrapping failed")
|
|
}
|
|
return mtimeFs.load(file)
|
|
}
|