Repository: spf13/afero
Pull request #279 (2020-12-07 13:23:26)

Implement chown functionality
Fixed merge issues of the original patch author @bohdanlisovskyi.


Patch:
diff --git a/.travis.yml b/.travis.yml
index 45bc131..8156c4e 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -7,6 +7,7 @@ arch:
 go:
   - "1.13"
   - "1.14"
+  - "1.15"
   - tip
 
 os:
diff --git a/README.md b/README.md
index c3e807a..1400bc4 100644
--- a/README.md
+++ b/README.md
@@ -94,6 +94,7 @@ AppFs.Open('/tmp/foo')
 File System Methods Available:
 ```go
 Chmod(name string, mode os.FileMode) : error
+Chown(name string, uid, gid int) : error
 Chtimes(name string, atime time.Time, mtime time.Time) : error
 Create(name string) : File, error
 Mkdir(name string, perm os.FileMode) : error
diff --git a/afero.go b/afero.go
index f5b5e12..469ff7d 100644
--- a/afero.go
+++ b/afero.go
@@ -91,9 +91,12 @@ type Fs interface {
 	// The name of this FileSystem
 	Name() string
 
-	//Chmod changes the mode of the named file to mode.
+	// Chmod changes the mode of the named file to mode.
 	Chmod(name string, mode os.FileMode) error
 
+	// Chown changes the uid and gid of the named file.
+	Chown(name string, uid, gid int) error
+
 	//Chtimes changes the access and modification times of the named file
 	Chtimes(name string, atime time.Time, mtime time.Time) error
 }
diff --git a/basepath.go b/basepath.go
index 3a14b83..4f98328 100644
--- a/basepath.go
+++ b/basepath.go
@@ -83,6 +83,13 @@ func (b *BasePathFs) Chmod(name string, mode os.FileMode) (err error) {
 	return b.source.Chmod(name, mode)
 }
 
+func (b *BasePathFs) Chown(name string, uid, gid int) (err error) {
+	if name, err = b.RealPath(name); err != nil {
+		return &os.PathError{Op: "chown", Path: name, Err: err}
+	}
+	return b.source.Chown(name, uid, gid)
+}
+
 func (b *BasePathFs) Name() string {
 	return "BasePathFs"
 }
@@ -202,5 +209,3 @@ func (b *BasePathFs) ReadlinkIfPossible(name string) (string, error) {
 	}
 	return "", &os.PathError{Op: "readlink", Path: name, Err: ErrNoReadlink}
 }
-
-// vim: ts=4 sw=4 noexpandtab nolist syn=go
diff --git a/cacheOnReadFs.go b/cacheOnReadFs.go
index 29a26c6..71471aa 100644
--- a/cacheOnReadFs.go
+++ b/cacheOnReadFs.go
@@ -117,6 +117,27 @@ func (u *CacheOnReadFs) Chmod(name string, mode os.FileMode) error {
 	return u.layer.Chmod(name, mode)
 }
 
+func (u *CacheOnReadFs) Chown(name string, uid, gid int) error {
+	st, _, err := u.cacheStatus(name)
+	if err != nil {
+		return err
+	}
+	switch st {
+	case cacheLocal:
+	case cacheHit:
+		err = u.base.Chown(name, uid, gid)
+	case cacheStale, cacheMiss:
+		if err := u.copyToLayer(name); err != nil {
+			return err
+		}
+		err = u.base.Chown(name, uid, gid)
+	}
+	if err != nil {
+		return err
+	}
+	return u.layer.Chown(name, uid, gid)
+}
+
 func (u *CacheOnReadFs) Stat(name string) (os.FileInfo, error) {
 	st, fi, err := u.cacheStatus(name)
 	if err != nil {
diff --git a/copyOnWriteFs.go b/copyOnWriteFs.go
index 96b7701..6ff8f30 100644
--- a/copyOnWriteFs.go
+++ b/copyOnWriteFs.go
@@ -14,7 +14,7 @@ var _ Lstater = (*CopyOnWriteFs)(nil)
 // a possibly writeable layer on top. Changes to the file system will only
 // be made in the overlay: Changing an existing file in the base layer which
 // is not present in the overlay will copy the file to the overlay ("changing"
-// includes also calls to e.g. Chtimes() and Chmod()).
+// includes also calls to e.g. Chtimes(), Chmod() and Chown()).
 //
 // Reading directories is currently only supported via Open(), not OpenFile().
 type CopyOnWriteFs struct {
@@ -75,6 +75,19 @@ func (u *CopyOnWriteFs) Chmod(name string, mode os.FileMode) error {
 	return u.layer.Chmod(name, mode)
 }
 
+func (u *CopyOnWriteFs) Chown(name string, uid, gid int) error {
+	b, err := u.isBaseFile(name)
+	if err != nil {
+		return err
+	}
+	if b {
+		if err := u.copyToLayer(name); err != nil {
+			return err
+		}
+	}
+	return u.layer.Chown(name, uid, gid)
+}
+
 func (u *CopyOnWriteFs) Stat(name string) (os.FileInfo, error) {
 	fi, err := u.layer.Stat(name)
 	if err != nil {
diff --git a/httpFs.go b/httpFs.go
index c421936..2b86e30 100644
--- a/httpFs.go
+++ b/httpFs.go
@@ -67,6 +67,10 @@ func (h HttpFs) Chmod(name string, mode os.FileMode) error {
 	return h.source.Chmod(name, mode)
 }
 
+func (h HttpFs) Chown(name string, uid, gid int) error {
+	return h.source.Chown(name, uid, gid)
+}
+
 func (h HttpFs) Chtimes(name string, atime time.Time, mtime time.Time) error {
 	return h.source.Chtimes(name, atime, mtime)
 }
diff --git a/mem/file.go b/mem/file.go
index 122d77c..7c08522 100644
--- a/mem/file.go
+++ b/mem/file.go
@@ -57,6 +57,8 @@ type FileData struct {
 	dir     bool
 	mode    os.FileMode
 	modtime time.Time
+	uid     int
+	gid     int
 }
 
 func (d *FileData) Name() string {
@@ -95,6 +97,18 @@ func setModTime(f *FileData, mtime time.Time) {
 	f.modtime = mtime
 }
 
+func SetUID(f *FileData, uid int) {
+	f.Lock()
+	f.uid = uid
+	f.Unlock()
+}
+
+func SetGID(f *FileData, gid int) {
+	f.Lock()
+	f.uid = gid
+	f.Unlock()
+}
+
 func GetFileInfo(f *FileData) *FileInfo {
 	return &FileInfo{f}
 }
diff --git a/memmap.go b/memmap.go
index 0fa9592..5c265f9 100644
--- a/memmap.go
+++ b/memmap.go
@@ -363,6 +363,22 @@ func (m *MemMapFs) setFileMode(name string, mode os.FileMode) error {
 	return nil
 }
 
+func (m *MemMapFs) Chown(name string, uid, gid int) error {
+	name = normalizePath(name)
+
+	m.mu.RLock()
+	f, ok := m.getData()[name]
+	m.mu.RUnlock()
+	if !ok {
+		return &os.PathError{Op: "chown", Path: name, Err: ErrFileNotFound}
+	}
+
+	mem.SetUID(f, uid)
+	mem.SetGID(f, gid)
+
+	return nil
+}
+
 func (m *MemMapFs) Chtimes(name string, atime time.Time, mtime time.Time) error {
 	name = normalizePath(name)
 
@@ -386,9 +402,3 @@ func (m *MemMapFs) List() {
 		fmt.Println(x.Name(), y.Size())
 	}
 }
-
-// func debugMemMapList(fs Fs) {
-// 	if x, ok := fs.(*MemMapFs); ok {
-// 		x.List()
-// 	}
-// }
diff --git a/os.go b/os.go
index 4761db5..f136632 100644
--- a/os.go
+++ b/os.go
@@ -91,6 +91,10 @@ func (OsFs) Chmod(name string, mode os.FileMode) error {
 	return os.Chmod(name, mode)
 }
 
+func (OsFs) Chown(name string, uid, gid int) error {
+	return os.Chown(name, uid, gid)
+}
+
 func (OsFs) Chtimes(name string, atime time.Time, mtime time.Time) error {
 	return os.Chtimes(name, atime, mtime)
 }
diff --git a/readonlyfs.go b/readonlyfs.go
index f94b181..bd8f926 100644
--- a/readonlyfs.go
+++ b/readonlyfs.go
@@ -28,6 +28,10 @@ func (r *ReadOnlyFs) Chmod(n string, m os.FileMode) error {
 	return syscall.EPERM
 }
 
+func (r *ReadOnlyFs) Chown(n string, uid, gid int) error {
+	return syscall.EPERM
+}
+
 func (r *ReadOnlyFs) Name() string {
 	return "ReadOnlyFilter"
 }
diff --git a/regexpfs.go b/regexpfs.go
index c8fc008..ac359c6 100644
--- a/regexpfs.go
+++ b/regexpfs.go
@@ -60,6 +60,13 @@ func (r *RegexpFs) Chmod(name string, mode os.FileMode) error {
 	return r.source.Chmod(name, mode)
 }
 
+func (r *RegexpFs) Chown(name string, uid, gid int) error {
+	if err := r.dirOrMatches(name); err != nil {
+		return err
+	}
+	return r.source.Chown(name, uid, gid)
+}
+
 func (r *RegexpFs) Name() string {
 	return "RegexpFs"
 }
diff --git a/sftpfs/sftp.go b/sftpfs/sftp.go
index e41df68..c66ab47 100644
--- a/sftpfs/sftp.go
+++ b/sftpfs/sftp.go
@@ -130,6 +130,10 @@ func (s Fs) Chmod(name string, mode os.FileMode) error {
 	return s.client.Chmod(name, mode)
 }
 
+func (s Fs) Chown(name string, uid, gid int) error {
+	return s.client.Chown(name, uid, gid)
+}
+
 func (s Fs) Chtimes(name string, atime time.Time, mtime time.Time) error {
 	return s.client.Chtimes(name, atime, mtime)
 }
diff --git a/tarfs/fs.go b/tarfs/fs.go
index f8a2ce2..0d764e5 100644
--- a/tarfs/fs.go
+++ b/tarfs/fs.go
@@ -134,4 +134,6 @@ func (fs *Fs) Stat(name string) (os.FileInfo, error) {
 
 func (fs *Fs) Chmod(name string, mode os.FileMode) error { return syscall.EROFS }
 
+func (fs *Fs) Chown(name string, uid, gid int) error { return syscall.EROFS }
+
 func (fs *Fs) Chtimes(name string, atime time.Time, mtime time.Time) error { return syscall.EROFS }
diff --git a/zipfs/fs.go b/zipfs/fs.go
index 72887f9..d08b7f5 100644
--- a/zipfs/fs.go
+++ b/zipfs/fs.go
@@ -108,4 +108,6 @@ func (fs *Fs) Name() string { return "zipfs" }
 
 func (fs *Fs) Chmod(name string, mode os.FileMode) error { return syscall.EPERM }
 
+func (fs *Fs) Chown(name string, uid, gid int) error { return syscall.EPERM }
+
 func (fs *Fs) Chtimes(name string, atime time.Time, mtime time.Time) error { return syscall.EPERM }


Test patch:
diff --git a/memmap_test.go b/memmap_test.go
index 476909d..627b106 100644
--- a/memmap_test.go
+++ b/memmap_test.go
@@ -38,6 +38,8 @@ func TestPathErrors(t *testing.T) {
 	path2 := filepath.Join(".", "different", "path")
 	fs := NewMemMapFs()
 	perm := os.FileMode(0755)
+	uid := 1000
+	gid := 1000
 
 	// relevant functions:
 	// func (m *MemMapFs) Chmod(name string, mode os.FileMode) error
@@ -54,6 +56,9 @@ func TestPathErrors(t *testing.T) {
 	err := fs.Chmod(path, perm)
 	checkPathError(t, err, "Chmod")
 
+	err = fs.Chown(path, uid, gid)
+	checkPathError(t, err, "Chown")
+
 	err = fs.Chtimes(path, time.Now(), time.Now())
 	checkPathError(t, err, "Chtimes")