Merge pull request #4812 from kzys/freebsd-mount
FreeBSD support on pkg/mount
This commit is contained in:
commit
1b4ca55744
12 changed files with 350 additions and 147 deletions
62
mount/flags.go
Normal file
62
mount/flags.go
Normal file
|
@ -0,0 +1,62 @@
|
|||
package mount
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Parse fstab type mount options into mount() flags
|
||||
// and device specific data
|
||||
func parseOptions(options string) (int, string) {
|
||||
var (
|
||||
flag int
|
||||
data []string
|
||||
)
|
||||
|
||||
flags := map[string]struct {
|
||||
clear bool
|
||||
flag int
|
||||
}{
|
||||
"defaults": {false, 0},
|
||||
"ro": {false, RDONLY},
|
||||
"rw": {true, RDONLY},
|
||||
"suid": {true, NOSUID},
|
||||
"nosuid": {false, NOSUID},
|
||||
"dev": {true, NODEV},
|
||||
"nodev": {false, NODEV},
|
||||
"exec": {true, NOEXEC},
|
||||
"noexec": {false, NOEXEC},
|
||||
"sync": {false, SYNCHRONOUS},
|
||||
"async": {true, SYNCHRONOUS},
|
||||
"dirsync": {false, DIRSYNC},
|
||||
"remount": {false, REMOUNT},
|
||||
"mand": {false, MANDLOCK},
|
||||
"nomand": {true, MANDLOCK},
|
||||
"atime": {true, NOATIME},
|
||||
"noatime": {false, NOATIME},
|
||||
"diratime": {true, NODIRATIME},
|
||||
"nodiratime": {false, NODIRATIME},
|
||||
"bind": {false, BIND},
|
||||
"rbind": {false, RBIND},
|
||||
"private": {false, PRIVATE},
|
||||
"relatime": {false, RELATIME},
|
||||
"norelatime": {true, RELATIME},
|
||||
"strictatime": {false, STRICTATIME},
|
||||
"nostrictatime": {true, STRICTATIME},
|
||||
}
|
||||
|
||||
for _, o := range strings.Split(options, ",") {
|
||||
// If the option does not exist in the flags table or the flag
|
||||
// is not supported on the platform,
|
||||
// then it is a data value for a specific fs type
|
||||
if f, exists := flags[o]; exists && f.flag != 0 {
|
||||
if f.clear {
|
||||
flag &= ^f.flag
|
||||
} else {
|
||||
flag |= f.flag
|
||||
}
|
||||
} else {
|
||||
data = append(data, o)
|
||||
}
|
||||
}
|
||||
return flag, strings.Join(data, ",")
|
||||
}
|
28
mount/flags_freebsd.go
Normal file
28
mount/flags_freebsd.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
// +build freebsd,cgo
|
||||
|
||||
package mount
|
||||
|
||||
/*
|
||||
#include <sys/mount.h>
|
||||
*/
|
||||
import "C"
|
||||
|
||||
const (
|
||||
RDONLY = C.MNT_RDONLY
|
||||
NOSUID = C.MNT_NOSUID
|
||||
NOEXEC = C.MNT_NOEXEC
|
||||
SYNCHRONOUS = C.MNT_SYNCHRONOUS
|
||||
NOATIME = C.MNT_NOATIME
|
||||
|
||||
BIND = 0
|
||||
DIRSYNC = 0
|
||||
MANDLOCK = 0
|
||||
NODEV = 0
|
||||
NODIRATIME = 0
|
||||
PRIVATE = 0
|
||||
RBIND = 0
|
||||
RELATIVE = 0
|
||||
RELATIME = 0
|
||||
REMOUNT = 0
|
||||
STRICTATIME = 0
|
||||
)
|
|
@ -3,62 +3,23 @@
|
|||
package mount
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// Parse fstab type mount options into mount() flags
|
||||
// and device specific data
|
||||
func parseOptions(options string) (int, string) {
|
||||
var (
|
||||
flag int
|
||||
data []string
|
||||
)
|
||||
|
||||
flags := map[string]struct {
|
||||
clear bool
|
||||
flag int
|
||||
}{
|
||||
"defaults": {false, 0},
|
||||
"ro": {false, syscall.MS_RDONLY},
|
||||
"rw": {true, syscall.MS_RDONLY},
|
||||
"suid": {true, syscall.MS_NOSUID},
|
||||
"nosuid": {false, syscall.MS_NOSUID},
|
||||
"dev": {true, syscall.MS_NODEV},
|
||||
"nodev": {false, syscall.MS_NODEV},
|
||||
"exec": {true, syscall.MS_NOEXEC},
|
||||
"noexec": {false, syscall.MS_NOEXEC},
|
||||
"sync": {false, syscall.MS_SYNCHRONOUS},
|
||||
"async": {true, syscall.MS_SYNCHRONOUS},
|
||||
"dirsync": {false, syscall.MS_DIRSYNC},
|
||||
"remount": {false, syscall.MS_REMOUNT},
|
||||
"mand": {false, syscall.MS_MANDLOCK},
|
||||
"nomand": {true, syscall.MS_MANDLOCK},
|
||||
"atime": {true, syscall.MS_NOATIME},
|
||||
"noatime": {false, syscall.MS_NOATIME},
|
||||
"diratime": {true, syscall.MS_NODIRATIME},
|
||||
"nodiratime": {false, syscall.MS_NODIRATIME},
|
||||
"bind": {false, syscall.MS_BIND},
|
||||
"rbind": {false, syscall.MS_BIND | syscall.MS_REC},
|
||||
"private": {false, syscall.MS_PRIVATE},
|
||||
"relatime": {false, syscall.MS_RELATIME},
|
||||
"norelatime": {true, syscall.MS_RELATIME},
|
||||
"strictatime": {false, syscall.MS_STRICTATIME},
|
||||
"nostrictatime": {true, syscall.MS_STRICTATIME},
|
||||
}
|
||||
|
||||
for _, o := range strings.Split(options, ",") {
|
||||
// If the option does not exist in the flags table then it is a
|
||||
// data value for a specific fs type
|
||||
if f, exists := flags[o]; exists {
|
||||
if f.clear {
|
||||
flag &= ^f.flag
|
||||
} else {
|
||||
flag |= f.flag
|
||||
}
|
||||
} else {
|
||||
data = append(data, o)
|
||||
}
|
||||
}
|
||||
return flag, strings.Join(data, ",")
|
||||
}
|
||||
const (
|
||||
RDONLY = syscall.MS_RDONLY
|
||||
NOSUID = syscall.MS_NOSUID
|
||||
NODEV = syscall.MS_NODEV
|
||||
NOEXEC = syscall.MS_NOEXEC
|
||||
SYNCHRONOUS = syscall.MS_SYNCHRONOUS
|
||||
DIRSYNC = syscall.MS_DIRSYNC
|
||||
REMOUNT = syscall.MS_REMOUNT
|
||||
MANDLOCK = syscall.MS_MANDLOCK
|
||||
NOATIME = syscall.MS_NOATIME
|
||||
NODIRATIME = syscall.MS_NODIRATIME
|
||||
BIND = syscall.MS_BIND
|
||||
RBIND = syscall.MS_BIND | syscall.MS_REC
|
||||
PRIVATE = syscall.MS_PRIVATE
|
||||
RELATIME = syscall.MS_RELATIME
|
||||
STRICTATIME = syscall.MS_STRICTATIME
|
||||
)
|
||||
|
|
|
@ -1,7 +1,22 @@
|
|||
// +build !linux !amd64
|
||||
// +build !linux,!freebsd linux,!amd64 freebsd,!cgo
|
||||
|
||||
package mount
|
||||
|
||||
func parseOptions(options string) (int, string) {
|
||||
panic("Not implemented")
|
||||
}
|
||||
const (
|
||||
BIND = 0
|
||||
DIRSYNC = 0
|
||||
MANDLOCK = 0
|
||||
NOATIME = 0
|
||||
NODEV = 0
|
||||
NODIRATIME = 0
|
||||
NOEXEC = 0
|
||||
NOSUID = 0
|
||||
PRIVATE = 0
|
||||
RBIND = 0
|
||||
RELATIME = 0
|
||||
RELATIVE = 0
|
||||
REMOUNT = 0
|
||||
STRICTATIME = 0
|
||||
SYNCHRONOUS = 0
|
||||
RDONLY = 0
|
||||
)
|
||||
|
|
|
@ -3,12 +3,11 @@ package mount
|
|||
import (
|
||||
"os"
|
||||
"path"
|
||||
"syscall"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMountOptionsParsing(t *testing.T) {
|
||||
options := "bind,ro,size=10k"
|
||||
options := "noatime,ro,size=10k"
|
||||
|
||||
flag, data := parseOptions(options)
|
||||
|
||||
|
@ -16,7 +15,7 @@ func TestMountOptionsParsing(t *testing.T) {
|
|||
t.Fatalf("Expected size=10 got %s", data)
|
||||
}
|
||||
|
||||
expectedFlag := syscall.MS_BIND | syscall.MS_RDONLY
|
||||
expectedFlag := NOATIME | RDONLY
|
||||
|
||||
if flag != expectedFlag {
|
||||
t.Fatalf("Expected %d got %d", expectedFlag, flag)
|
||||
|
@ -31,10 +30,15 @@ func TestMounted(t *testing.T) {
|
|||
defer os.RemoveAll(tmp)
|
||||
|
||||
var (
|
||||
sourcePath = path.Join(tmp, "sourcefile.txt")
|
||||
targetPath = path.Join(tmp, "targetfile.txt")
|
||||
sourceDir = path.Join(tmp, "source")
|
||||
targetDir = path.Join(tmp, "target")
|
||||
sourcePath = path.Join(sourceDir, "file.txt")
|
||||
targetPath = path.Join(targetDir, "file.txt")
|
||||
)
|
||||
|
||||
os.Mkdir(sourceDir, 0777)
|
||||
os.Mkdir(targetDir, 0777)
|
||||
|
||||
f, err := os.Create(sourcePath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -48,23 +52,23 @@ func TestMounted(t *testing.T) {
|
|||
}
|
||||
f.Close()
|
||||
|
||||
if err := Mount(sourcePath, targetPath, "none", "bind,rw"); err != nil {
|
||||
if err := Mount(sourceDir, targetDir, "none", "bind,rw"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := Unmount(targetPath); err != nil {
|
||||
if err := Unmount(targetDir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
mounted, err := Mounted(targetPath)
|
||||
mounted, err := Mounted(targetDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !mounted {
|
||||
t.Fatalf("Expected %s to be mounted", targetPath)
|
||||
t.Fatalf("Expected %s to be mounted", targetDir)
|
||||
}
|
||||
if _, err := os.Stat(targetPath); err != nil {
|
||||
if _, err := os.Stat(targetDir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
@ -77,10 +81,15 @@ func TestMountReadonly(t *testing.T) {
|
|||
defer os.RemoveAll(tmp)
|
||||
|
||||
var (
|
||||
sourcePath = path.Join(tmp, "sourcefile.txt")
|
||||
targetPath = path.Join(tmp, "targetfile.txt")
|
||||
sourceDir = path.Join(tmp, "source")
|
||||
targetDir = path.Join(tmp, "target")
|
||||
sourcePath = path.Join(sourceDir, "file.txt")
|
||||
targetPath = path.Join(targetDir, "file.txt")
|
||||
)
|
||||
|
||||
os.Mkdir(sourceDir, 0777)
|
||||
os.Mkdir(targetDir, 0777)
|
||||
|
||||
f, err := os.Create(sourcePath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -94,11 +103,11 @@ func TestMountReadonly(t *testing.T) {
|
|||
}
|
||||
f.Close()
|
||||
|
||||
if err := Mount(sourcePath, targetPath, "none", "bind,ro"); err != nil {
|
||||
if err := Mount(sourceDir, targetDir, "none", "bind,ro"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := Unmount(targetPath); err != nil {
|
||||
if err := Unmount(targetDir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
@ -108,3 +117,21 @@ func TestMountReadonly(t *testing.T) {
|
|||
t.Fatal("Should not be able to open a ro file as rw")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetMounts(t *testing.T) {
|
||||
mounts, err := GetMounts()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
root := false
|
||||
for _, entry := range mounts {
|
||||
if entry.Mountpoint == "/" {
|
||||
root = true
|
||||
}
|
||||
}
|
||||
|
||||
if !root {
|
||||
t.Fatal("/ should be mounted at least")
|
||||
}
|
||||
}
|
||||
|
|
59
mount/mounter_freebsd.go
Normal file
59
mount/mounter_freebsd.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
package mount
|
||||
|
||||
/*
|
||||
#include <errno.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/_iovec.h>
|
||||
#include <sys/mount.h>
|
||||
#include <sys/param.h>
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func allocateIOVecs(options []string) []C.struct_iovec {
|
||||
out := make([]C.struct_iovec, len(options))
|
||||
for i, option := range options {
|
||||
out[i].iov_base = unsafe.Pointer(C.CString(option))
|
||||
out[i].iov_len = C.size_t(len(option) + 1)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func mount(device, target, mType string, flag uintptr, data string) error {
|
||||
isNullFS := false
|
||||
|
||||
xs := strings.Split(data, ",")
|
||||
for _, x := range xs {
|
||||
if x == "bind" {
|
||||
isNullFS = true
|
||||
}
|
||||
}
|
||||
|
||||
options := []string{"fspath", target}
|
||||
if isNullFS {
|
||||
options = append(options, "fstype", "nullfs", "target", device)
|
||||
} else {
|
||||
options = append(options, "fstype", mType, "from", device)
|
||||
}
|
||||
rawOptions := allocateIOVecs(options)
|
||||
for _, rawOption := range rawOptions {
|
||||
defer C.free(rawOption.iov_base)
|
||||
}
|
||||
|
||||
if errno := C.nmount(&rawOptions[0], C.uint(len(options)), C.int(flag)); errno != 0 {
|
||||
reason := C.GoString(C.strerror(*C.__error()))
|
||||
return fmt.Errorf("Failed to call nmount: %s", reason)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func unmount(target string, flag int) error {
|
||||
return syscall.Unmount(target, flag)
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
// +build !linux !amd64
|
||||
// +build !linux,!freebsd linux,!amd64 freebsd,!cgo
|
||||
|
||||
package mount
|
||||
|
||||
|
|
|
@ -1,79 +1,7 @@
|
|||
package mount
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
/* 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue
|
||||
(1)(2)(3) (4) (5) (6) (7) (8) (9) (10) (11)
|
||||
|
||||
(1) mount ID: unique identifier of the mount (may be reused after umount)
|
||||
(2) parent ID: ID of parent (or of self for the top of the mount tree)
|
||||
(3) major:minor: value of st_dev for files on filesystem
|
||||
(4) root: root of the mount within the filesystem
|
||||
(5) mount point: mount point relative to the process's root
|
||||
(6) mount options: per mount options
|
||||
(7) optional fields: zero or more fields of the form "tag[:value]"
|
||||
(8) separator: marks the end of the optional fields
|
||||
(9) filesystem type: name of filesystem of the form "type[.subtype]"
|
||||
(10) mount source: filesystem specific information or "none"
|
||||
(11) super options: per super block options*/
|
||||
mountinfoFormat = "%d %d %d:%d %s %s %s "
|
||||
)
|
||||
|
||||
type MountInfo struct {
|
||||
Id, Parent, Major, Minor int
|
||||
Root, Mountpoint, Opts string
|
||||
Fstype, Source, VfsOpts string
|
||||
}
|
||||
|
||||
// Parse /proc/self/mountinfo because comparing Dev and ino does not work from bind mounts
|
||||
func parseMountTable() ([]*MountInfo, error) {
|
||||
f, err := os.Open("/proc/self/mountinfo")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
return parseInfoFile(f)
|
||||
}
|
||||
|
||||
func parseInfoFile(r io.Reader) ([]*MountInfo, error) {
|
||||
var (
|
||||
s = bufio.NewScanner(r)
|
||||
out = []*MountInfo{}
|
||||
)
|
||||
|
||||
for s.Scan() {
|
||||
if err := s.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var (
|
||||
p = &MountInfo{}
|
||||
text = s.Text()
|
||||
)
|
||||
|
||||
if _, err := fmt.Sscanf(text, mountinfoFormat,
|
||||
&p.Id, &p.Parent, &p.Major, &p.Minor,
|
||||
&p.Root, &p.Mountpoint, &p.Opts); err != nil {
|
||||
return nil, fmt.Errorf("Scanning '%s' failed: %s", text, err)
|
||||
}
|
||||
// Safe as mountinfo encodes mountpoints with spaces as \040.
|
||||
index := strings.Index(text, " - ")
|
||||
postSeparatorFields := strings.Fields(text[index+3:])
|
||||
if len(postSeparatorFields) != 3 {
|
||||
return nil, fmt.Errorf("Error did not find 3 fields post '-' in '%s'", text)
|
||||
}
|
||||
p.Fstype = postSeparatorFields[0]
|
||||
p.Source = postSeparatorFields[1]
|
||||
p.VfsOpts = postSeparatorFields[2]
|
||||
out = append(out, p)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
|
38
mount/mountinfo_freebsd.go
Normal file
38
mount/mountinfo_freebsd.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
package mount
|
||||
|
||||
/*
|
||||
#include <sys/param.h>
|
||||
#include <sys/ucred.h>
|
||||
#include <sys/mount.h>
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// Parse /proc/self/mountinfo because comparing Dev and ino does not work from bind mounts
|
||||
func parseMountTable() ([]*MountInfo, error) {
|
||||
var rawEntries *C.struct_statfs
|
||||
|
||||
count := int(C.getmntinfo(&rawEntries, C.MNT_WAIT))
|
||||
if count == 0 {
|
||||
return nil, fmt.Errorf("Failed to call getmntinfo")
|
||||
}
|
||||
|
||||
var entries []C.struct_statfs
|
||||
header := (*reflect.SliceHeader)(unsafe.Pointer(&entries))
|
||||
header.Cap = count
|
||||
header.Len = count
|
||||
header.Data = uintptr(unsafe.Pointer(rawEntries))
|
||||
|
||||
var out []*MountInfo
|
||||
for _, entry := range entries {
|
||||
var mountinfo MountInfo
|
||||
mountinfo.Mountpoint = C.GoString(&entry.f_mntonname[0])
|
||||
out = append(out, &mountinfo)
|
||||
}
|
||||
return out, nil
|
||||
}
|
73
mount/mountinfo_linux.go
Normal file
73
mount/mountinfo_linux.go
Normal file
|
@ -0,0 +1,73 @@
|
|||
package mount
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
/* 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue
|
||||
(1)(2)(3) (4) (5) (6) (7) (8) (9) (10) (11)
|
||||
|
||||
(1) mount ID: unique identifier of the mount (may be reused after umount)
|
||||
(2) parent ID: ID of parent (or of self for the top of the mount tree)
|
||||
(3) major:minor: value of st_dev for files on filesystem
|
||||
(4) root: root of the mount within the filesystem
|
||||
(5) mount point: mount point relative to the process's root
|
||||
(6) mount options: per mount options
|
||||
(7) optional fields: zero or more fields of the form "tag[:value]"
|
||||
(8) separator: marks the end of the optional fields
|
||||
(9) filesystem type: name of filesystem of the form "type[.subtype]"
|
||||
(10) mount source: filesystem specific information or "none"
|
||||
(11) super options: per super block options*/
|
||||
mountinfoFormat = "%d %d %d:%d %s %s %s "
|
||||
)
|
||||
|
||||
// Parse /proc/self/mountinfo because comparing Dev and ino does not work from bind mounts
|
||||
func parseMountTable() ([]*MountInfo, error) {
|
||||
f, err := os.Open("/proc/self/mountinfo")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
return parseInfoFile(f)
|
||||
}
|
||||
|
||||
func parseInfoFile(r io.Reader) ([]*MountInfo, error) {
|
||||
var (
|
||||
s = bufio.NewScanner(r)
|
||||
out = []*MountInfo{}
|
||||
)
|
||||
|
||||
for s.Scan() {
|
||||
if err := s.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var (
|
||||
p = &MountInfo{}
|
||||
text = s.Text()
|
||||
)
|
||||
|
||||
if _, err := fmt.Sscanf(text, mountinfoFormat,
|
||||
&p.Id, &p.Parent, &p.Major, &p.Minor,
|
||||
&p.Root, &p.Mountpoint, &p.Opts); err != nil {
|
||||
return nil, fmt.Errorf("Scanning '%s' failed: %s", text, err)
|
||||
}
|
||||
// Safe as mountinfo encodes mountpoints with spaces as \040.
|
||||
index := strings.Index(text, " - ")
|
||||
postSeparatorFields := strings.Fields(text[index+3:])
|
||||
if len(postSeparatorFields) != 3 {
|
||||
return nil, fmt.Errorf("Error did not find 3 fields post '-' in '%s'", text)
|
||||
}
|
||||
p.Fstype = postSeparatorFields[0]
|
||||
p.Source = postSeparatorFields[1]
|
||||
p.VfsOpts = postSeparatorFields[2]
|
||||
out = append(out, p)
|
||||
}
|
||||
return out, nil
|
||||
}
|
12
mount/mountinfo_unsupported.go
Normal file
12
mount/mountinfo_unsupported.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
// +build !linux,!freebsd freebsd,!cgo
|
||||
|
||||
package mount
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func parseMountTable() ([]*MountInfo, error) {
|
||||
return nil, fmt.Errorf("mount.parseMountTable is not implemented on %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||
}
|
Loading…
Reference in a new issue