diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index cf84c8028..1804457cf 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -21,8 +21,8 @@ jobs: - job_name: linux os: ubuntu-latest containerName: 'test-cnt-ubn' - fuselib: libfuse-dev - fuselib2: fuse + fuselib: libfuse3-dev + fuselib2: fuse3 runs-on: ${{ matrix.os }} env: @@ -50,8 +50,7 @@ jobs: - name: Install libfuse run: |- - sudo apt-get update --fix-missing -o Dpkg::Options::="--force-confnew" - sudo apt-get install make cmake gcc g++ parallel ${{ matrix.fuselib }} ${{ matrix.fuselib2 }} -y -o Dpkg::Options::="--force-confnew" + sudo apt-get install ${{ matrix.fuselib }} ${{ matrix.fuselib2 }} - name: Create Directory Structure run: |- @@ -1012,8 +1011,8 @@ jobs: - job_name: linux os: ubuntu-latest containerName: 'test-cnt-ubn' - fuselib: libfuse-dev - fuselib2: fuse + fuselib: libfuse3-dev + fuselib2: fuse3 runs-on: ${{ matrix.os }} timeout-minutes: 30 @@ -1030,8 +1029,7 @@ jobs: - name: Install dependency run: |- - sudo apt-get update --fix-missing -o Dpkg::Options::="--force-confnew" - sudo apt-get install make cmake gcc g++ parallel ${{ matrix.fuselib }} ${{ matrix.fuselib2 }} -y -o Dpkg::Options::="--force-confnew" + sudo apt-get install ${{ matrix.fuselib }} ${{ matrix.fuselib2 }} - name: Cleanup Blob Storage run: go test -timeout 120m -v test/accoutcleanup/accountcleanup_test.go diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 2e52ad048..a3736b960 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -213,14 +213,19 @@ jobs: zig: 0.13.0 steps: - - # libfuse-dev is required to build our command-line program + - name: Add arm64 repository + run: | + echo -e "deb [arch=arm64] http://ports.ubuntu.com/ jammy main restricted\n" "deb [arch=arm64] http://ports.ubuntu.com/ jammy-updates main restricted\n" "deb [arch=arm64] http://ports.ubuntu.com/ jammy universe\n" "deb [arch=arm64] http://ports.ubuntu.com/ jammy-updates universe\n" "deb [arch=arm64] http://ports.ubuntu.com/ jammy multiverse\n" "deb [arch=arm64] http://ports.ubuntu.com/ jammy-updates multiverse\n" "deb [arch=arm64] http://ports.ubuntu.com/ jammy-backports main restricted universe multiverse" | sudo tee /etc/apt/sources.list.d/arm-cross-compile-sources.list + sudo dpkg --add-architecture arm64 + sudo apt-get update + - # libfuse3-dev is required to build our command-line program name: Install Libfuse run: | - sudo apt-get install -y libfuse-dev + sudo apt-get install -y libfuse3-dev - # enable GoReleaser to build for ARM64 name: Install ARM64 compilers run: | - sudo apt-get install -y gcc-aarch64-linux-gnu + sudo apt-get install -y gcc-aarch64-linux-gnu libfuse3-dev:arm64 - # Get code and Go ready name: Checkout code uses: actions/checkout@v4 diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index b82788586..ce62b77e9 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -55,7 +55,7 @@ jobs: - name: Install libfuse on Linux shell: bash run: | - sudo apt-get install fuse3 libfuse-dev rpm pkg-config + sudo apt-get install fuse3 libfuse3-dev rpm pkg-config - name: Build run: | @@ -169,7 +169,7 @@ jobs: - name: Install libfuse on Linux shell: bash run: | - sudo apt-get install libfuse-dev + sudo apt-get install libfuse3-dev - name: golangci-lint uses: golangci/golangci-lint-action@v6 diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 18cf65d13..fc3818b44 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -64,13 +64,14 @@ builds: - amd64 env: - CGO_ENABLED=1 - - CC=zig cc -target x86_64-linux-gnu - - CXX=zig c++ -target x86_64-linux-gnu + - CGO_LDFLAGS=-L/usr/lib/x86_64-linux-gnu + - CC=zig cc -target x86_64-linux-gnu -isystem /usr/lib/x86_64-linux-gnu -iwithsysroot /usr/include + - CXX=zig c++ -target x86_64-linux-gnu -isystem /usr/lib/x86_64-linux-gnu -iwithsysroot /usr/include flags: - -trimpath mod_timestamp: '{{ .CommitTimestamp }}' ldflags: - - -s -w -X github.com/Seagate/cloudfuse/common.GitCommit={{.Commit}} -X github.com/Seagate/cloudfuse/common.CommitDate={{ .CommitDate }} + - -s -w -X github.com/Seagate/cloudfuse/common.GitCommit={{.Commit}} -X github.com/Seagate/cloudfuse/common.CommitDate={{ .CommitDate }} -L /usr/lib/x86_64-linux-gnu - id: linux-amd64-health-monitor main: ./tools/health-monitor/main.go @@ -86,7 +87,7 @@ builds: mod_timestamp: '{{ .CommitTimestamp }}' ldflags: - -s -w -X github.com/Seagate/cloudfuse/common.GitCommit={{.Commit}} -X github.com/Seagate/cloudfuse/common.CommitDate={{ .CommitDate }} - + - id: linux-arm64 goos: - linux @@ -94,8 +95,9 @@ builds: - arm64 env: - CGO_ENABLED=1 - - CC=zig cc -target aarch64-linux-gnu - - CXX=zig c++ -target aarch64-linux-gnu + - CGO_LDFLAGS=-L/usr/lib/aarch64-linux-gnu + - CC=zig cc -target aarch64-linux-gnu -isystem /usr/lib/aarch64-linux-gnu -iwithsysroot /usr/include + - CXX=zig c++ -target aarch64-linux-gnu -isystem /usr/lib/aarch64-linux-gnu -iwithsysroot /usr/include flags: - -trimpath mod_timestamp: '{{ .CommitTimestamp }}' @@ -145,7 +147,7 @@ archives: dst: "./samples/sampleStreamingConfigS3.yaml" - src: "sampleStreamingConfigAzure.yaml" dst: "./samples/sampleStreamingConfigAzure.yaml" - + - id: linux-amd64_no_gui builds: - linux-amd64 @@ -168,7 +170,7 @@ archives: dst: "./samples/sampleStreamingConfigS3.yaml" - src: "sampleStreamingConfigAzure.yaml" dst: "./samples/sampleStreamingConfigAzure.yaml" - + - id: linux-arm64_no_gui builds: - linux-arm64 @@ -191,7 +193,7 @@ archives: dst: "./samples/sampleStreamingConfigS3.yaml" - src: "sampleStreamingConfigAzure.yaml" dst: "./samples/sampleStreamingConfigAzure.yaml" - + - id: windows builds: - windows @@ -216,7 +218,7 @@ archives: dst: "./samples/sampleStreamingConfigS3.yaml" - src: "sampleStreamingConfigAzure.yaml" dst: "./samples/sampleStreamingConfigAzure.yaml" - + - id: windows_no_gui builds: - windows @@ -240,6 +242,7 @@ archives: - src: "sampleStreamingConfigAzure.yaml" dst: "./samples/sampleStreamingConfigAzure.yaml" + release: extra_files: - glob: ./build/Output/cloudfuse_{{.Version}}_windows_amd64.exe @@ -305,11 +308,11 @@ nfpms: overrides: deb: dependencies: - - libfuse-dev + - libfuse3-dev rpm: dependencies: - - fuse-devel + - fuse3-devel - id: linux_no_gui @@ -367,11 +370,11 @@ nfpms: overrides: deb: dependencies: - - libfuse-dev + - libfuse3-dev rpm: dependencies: - - fuse-devel + - fuse3-devel metadata: mod_timestamp: "{{ .CommitTimestamp }}" diff --git a/TSG.md b/TSG.md index f1fb9fa56..64be88630 100644 --- a/TSG.md +++ b/TSG.md @@ -55,7 +55,6 @@ FUSE allows mounting filesystem in user space, and is only accessible by the use You try to unmount the blob storage, but the recommended command is not found. Whilst `umount` may work instead, fusermount is the recommended method, so install the fuse package, for example on Ubuntu 20+: sudo apt install fuse3 -please note the fuse version (2 or 3) is dependent on the linux distribution you're using. Refer to fuse version for your distro. **8. Hangs while mounting to private link storage account** @@ -249,27 +248,17 @@ For non-HNS accounts Cloudfuse expects special directory marker files to exist i **10. File size and LMT are updated but file contents are not refreshed** -Cloudfuse supports fuse2 compatible linux distros. In all linux distros kernel cached contents of file in its page-cache. As long as cache is valid read/write are served from cache and calls will not reach to file-system drivers (Cloudfuse in our case). This page-cache is invalidated when page is swapped-out, manually cleared by user through cli or file-system driver requests for it. - -In case of fuse2 compliant distros, libfuse does not support invalidating the page cache. Contents once cached will remain with kernel until user manually clears the page-cache or kernel decides to swap it out. This means even if the file size or LMT has changed and Cloudfuse decided to refresh the content by redownloading the file, on read user will still get the stale contents. - - - If user is observing that list or stat call to file shows updated time or size but contents are not reflecting accordingly, first confirm with Cloudfuse logs that file was indeed downloaded afresh. If file-cache-timeout has not expired then Cloudfuse will keep using the current version of file persisted on temp cache and contents will not be refreshed. If Cloudfuse has downloaded the latest file and user still observes stale contents then clear the kernel page-cache manually using ```sysctl -w vm.drop_caches=3``` command. - # Problems with build -Make sure you have correctly setup your GO dev environment. Ensure you have installed fuse2 for example: +Make sure you have correctly setup your GO dev environment. Ensure you have installed fuse3 for example: - sudo apt-get install fuse libfuse-dev -y + sudo apt-get install fuse3 libfuse3-dev -y diff --git a/build.sh b/build.sh index d8708aade..ef1c40a0a 100755 --- a/build.sh +++ b/build.sh @@ -1,12 +1,19 @@ #!/bin/bash if [[ "$OSTYPE" == "linux-gnu"* ]]; then + if [ "$1" == "fuse2" ] + then + rm -rf cloudfuse + # Build cloudfuse with fuse2 + go build -o cloudfuse -tags fuse2 + else + rm -rf cloudfuse + # Build cloudfuse with fuse3 + go build -o cloudfuse + fi + # Build Health Monitor binary rm -rf cfusemon go build -o cfusemon ./tools/health-monitor/ - - # Build cloudfuse - rm -rf cloudfuse - go build -o cloudfuse else rm -rf cfusemon go build -o cfusemon.exe ./tools/health-monitor/ diff --git a/common/util.go b/common/util.go index 4a4dfff59..1d8ee0498 100644 --- a/common/util.go +++ b/common/util.go @@ -369,3 +369,36 @@ func IsDriveLetter(path string) bool { match, _ := regexp.MatchString(pattern, path) return match } + +func GetFuseMinorVersion() int { + var out bytes.Buffer + cmd := exec.Command("fusermount3", "--version") + cmd.Stdout = &out + + err := cmd.Run() + if err != nil { + return 0 + } + + output := strings.Split(out.String(), ":") + if len(output) < 2 { + return 0 + } + + version := strings.Trim(output[1], " ") + if version == "" { + return 0 + } + + output = strings.Split(version, ".") + if len(output) < 2 { + return 0 + } + + val, err := strconv.Atoi(output[1]) + if err != nil { + return 0 + } + + return val +} diff --git a/component/libfuse/extension_handler.h b/component/libfuse/extension_handler.h new file mode 100644 index 000000000..b70debda9 --- /dev/null +++ b/component/libfuse/extension_handler.h @@ -0,0 +1,139 @@ +/* + Licensed under the MIT License . + + Copyright © 2023-2024 Seagate Technology LLC and/or its Affiliates + Copyright © 2020-2024 Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE +*/ + +#ifndef __EXTENSION_HANDLER_H__ +#define __EXTENSION_HANDLER_H__ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +// ------------------------------------------------------------------------------------------------------------- +// Extension loading and registration methods +static void *extHandle = NULL; +typedef int (*callback_exchanger)(struct fuse_operations *opts); +typedef const char *(*lib_validator)(const char *sign); +typedef int (*lib_initializer)(const char *conf_file); + +static callback_exchanger ext_fuse_regsiter_func = NULL; +static callback_exchanger ext_storage_regsiter_func = NULL; + +static int load_library(char *extension_path) +{ + // Load the configured library here + extHandle = dlopen(extension_path, RTLD_LAZY); + if (extHandle == NULL) + { + return 1; + } + + // Get the function pointers from the lib and store them in given structure + // Once we register these methods to libfuse, calls will directly land into extension + lib_validator ext_lib_validator_func = NULL; + lib_initializer ext_lib_init_func = NULL; + + ext_fuse_regsiter_func = (callback_exchanger)dlsym(extHandle, "register_fuse_callbacks"); + ext_storage_regsiter_func = (callback_exchanger)dlsym(extHandle, "register_storage_callbacks"); + ext_lib_validator_func = (lib_validator)dlsym(extHandle, "validate_signature"); + ext_lib_init_func = (lib_initializer)dlsym(extHandle, "init_extension"); + + // Validate lib has legit functions exposed with this name + if (ext_fuse_regsiter_func == NULL || ext_storage_regsiter_func == NULL || + ext_lib_validator_func == NULL || ext_lib_init_func == NULL) + { + return 2; + } + +// Going for handshake with extension +#ifdef __FUSE2__ + const char *my_call_sign = "ola-amigo!!"; + const char *lib_call_sign = "ola-amigo!!!"; +#else + const char *my_call_sign = "ola-amigo-3!!"; + const char *lib_call_sign = "ola-amigo-3!!!"; +#endif + + const char *call_sign = ext_lib_validator_func(my_call_sign); + if (strcmp(call_sign, lib_call_sign) != 0) + { + return 3; + } + + if (0 != ext_lib_init_func("config.txt")) + { + return 4; + } + + return 0; +} + +static int unload_library() +{ + if (extHandle) + { + dlclose(extHandle); + } + return 0; +} + +static int get_extension_callbacks(fuse_operations_t *opt) +{ + if (!ext_fuse_regsiter_func) + { + return 1; + } + + if (0 != ext_fuse_regsiter_func(opt)) + { + return 2; + } + + return 0; +} + +static int register_callback_to_extension(fuse_operations_t *opt) +{ + if (!ext_storage_regsiter_func) + { + return 1; + } + + if (0 != ext_storage_regsiter_func(opt)) + { + return 2; + } + + return 0; +} + +// ------------------------------------------------------------------------------------------------------------- +#endif //__EXTENSION_HANDLER_H__ \ No newline at end of file diff --git a/component/libfuse/libfuse.go b/component/libfuse/libfuse.go index 4a064f2fd..660c9918f 100644 --- a/component/libfuse/libfuse.go +++ b/component/libfuse/libfuse.go @@ -36,8 +36,6 @@ import ( "github.com/Seagate/cloudfuse/common/log" "github.com/Seagate/cloudfuse/internal" "github.com/Seagate/cloudfuse/internal/stats_manager" - - "github.com/winfsp/cgofuse/fuse" ) /* NOTES: @@ -47,44 +45,6 @@ import ( - To read any new setting from config file follow the Configure method default comments */ -// Libfuse holds the settings and information for the FUSE component. -type Libfuse struct { - internal.BaseComponent - host *fuse.FileSystemHost - mountPath string - dirPermission uint - filePermission uint - readOnly bool - attributeExpiration uint32 - entryExpiration uint32 - negativeTimeout uint32 - allowOther bool - allowRoot bool - ownerUID uint32 - ownerGID uint32 - traceEnable bool - extensionPath string - disableWritebackCache bool - ignoreOpenFlags bool - nonEmptyMount bool - networkShare bool // Run as a network file share on Windows - lsFlags common.BitMap16 - maxFuseThreads uint32 - directIO bool - umask uint32 - displayCapacityMb uint64 -} - -// To support pagination in readdir calls this structure holds a block of items for a given directory -type dirChildCache struct { - sIndex uint64 // start index of current block of items - eIndex uint64 // End index of current block of items - length uint64 // Length of the children list - token string // Token to get next block of items from container - children []*internal.ObjAttr // Slice holding current block of children - lastPage bool // Whether current block is the last one -} - // LibfuseOptions defines the config parameters. type LibfuseOptions struct { mountPath string @@ -114,8 +74,6 @@ const defaultEntryExpiration = 120 const defaultAttrExpiration = 120 const defaultNegativeEntryExpiration = 120 const defaultMaxFuseThreads = 128 -const maxNameSize = 255 -const blockSize = 4096 var fuseFS *Libfuse diff --git a/component/libfuse/libfuse2_handler.go b/component/libfuse/libfuse2_handler.go index ee130c63e..153ef0d69 100644 --- a/component/libfuse/libfuse2_handler.go +++ b/component/libfuse/libfuse2_handler.go @@ -1,4 +1,4 @@ -package libfuse +//go:build windows || fuse2 /* Licensed under the MIT License . @@ -25,6 +25,8 @@ package libfuse SOFTWARE */ +package libfuse + import ( "errors" "fmt" @@ -68,6 +70,47 @@ type CgofuseFS struct { gid uint32 } +// Libfuse holds the settings and information for the FUSE component. +type Libfuse struct { + internal.BaseComponent + host *fuse.FileSystemHost + mountPath string + dirPermission uint + filePermission uint + readOnly bool + attributeExpiration uint32 + entryExpiration uint32 + negativeTimeout uint32 + allowOther bool + allowRoot bool + ownerUID uint32 + ownerGID uint32 + traceEnable bool + extensionPath string + disableWritebackCache bool + ignoreOpenFlags bool + nonEmptyMount bool + networkShare bool // Run as a network file share on Windows + lsFlags common.BitMap16 + maxFuseThreads uint32 + directIO bool + umask uint32 + displayCapacityMb uint64 +} + +// To support pagination in readdir calls this structure holds a block of items for a given directory +type dirChildCache struct { + sIndex uint64 // start index of current block of items + eIndex uint64 // End index of current block of items + length uint64 // Length of the children list + token string // Token to get next block of items from container + children []*internal.ObjAttr // Slice holding current block of children + lastPage bool // Whether current block is the last one +} + +const maxNameSize = 255 +const blockSize = 4096 + // Note: libfuse prepends "/" to the path. // TODO: Not sure if this is needed for cgofuse, will need to check // trimFusePath trims the first character from the path provided by libfuse diff --git a/component/libfuse/libfuse2_handler_test_wrapper.go b/component/libfuse/libfuse2_handler_test_wrapper.go index 05536b829..510bd9e3f 100644 --- a/component/libfuse/libfuse2_handler_test_wrapper.go +++ b/component/libfuse/libfuse2_handler_test_wrapper.go @@ -1,3 +1,5 @@ +//go:build windows || fuse2 + /* Licensed under the MIT License . diff --git a/component/libfuse/libfuse_defs.h b/component/libfuse/libfuse_defs.h new file mode 100644 index 000000000..1c1fd6d44 --- /dev/null +++ b/component/libfuse/libfuse_defs.h @@ -0,0 +1,147 @@ +/* + Licensed under the MIT License . + + Copyright © 2023-2024 Seagate Technology LLC and/or its Affiliates + Copyright © 2020-2024 Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE +*/ + +#ifndef __LIBFUSE_DEFS_H__ +#define __LIBFUSE_DEFS_H__ + +/* + NOTES: + 1. Every method or variable defined in this file has to be static otherwise compilation will + fail with multiple definition error + 2. Every method defined as static shall be defined in go code with //export before it + 3. No blank line between C code and import "C" statement anywhere + 4. For void* import unsafe in Go and use unsafe.Pointer + 5. For C types like int use C.int in Go + 6. import "C" and code that uses it has to be in same Go file +*/ + +typedef struct fuse_operations fuse_operations_t; +typedef struct fuse_conn_info fuse_conn_info_t; +typedef struct fuse_config fuse_config_t; +typedef struct fuse_args fuse_args_t; +typedef struct fuse_file_info fuse_file_info_t; +typedef struct statvfs statvfs_t; +typedef struct stat stat_t; +typedef struct timespec timespec_t; +typedef enum fuse_readdir_flags fuse_readdir_flags_t; +typedef enum fuse_fill_dir_flags fuse_fill_dir_flags_t; + +static int fill_dir_plus = FUSE_FILL_DIR_PLUS; + +// Structure to hold config for libfuse +typedef struct fuse_options +{ + char *mount_path; + uid_t uid; + gid_t gid; + mode_t permissions; + int entry_expiry; + int attr_expiry; + int negative_expiry; + bool readonly; + bool allow_other; + bool allow_root; + bool trace_enable; + bool non_empty; + int umask; +} fuse_options_t; + +// LibFuse callback declaration here +extern int libfuse_statfs(char *path, statvfs_t *stbuf); + +extern void libfuse_destroy(void *private_data); + +extern int libfuse_mkdir(char *path, mode_t mode); +extern int libfuse_rmdir(char *path); + +extern int libfuse_opendir(char *path, fuse_file_info_t *fi); +extern int libfuse_releasedir(char *path, fuse_file_info_t *fi); + +extern int libfuse_create(char *path, mode_t mode, fuse_file_info_t *fi); +extern int libfuse_open(char *path, fuse_file_info_t *fi); +extern int libfuse_read(char *path, char *buf, size_t size, off_t, fuse_file_info_t *fi); +extern int libfuse_write(char *path, char *buf, size_t size, off_t, fuse_file_info_t *fi); +extern int libfuse_flush(char *path, fuse_file_info_t *fi); +extern int libfuse_release(char *path, fuse_file_info_t *fi); +// truncate and rename is lib version specific so defined later +extern int libfuse_unlink(char *path); + +extern int libfuse_symlink(char *from, char *to); +extern int libfuse_readlink(char *path, char *buf, size_t size); + +extern int libfuse_fsync(char *path, int, fuse_file_info_t *fi); +extern int libfuse_fsyncdir(char *path, int, fuse_file_info_t *); + +// chmod, chown and utimens are lib version specific so defined later + +#ifdef __FUSE2__ +extern void *libfuse2_init(fuse_conn_info_t *conn); +extern int libfuse2_getattr(char *path, stat_t *stbuf); +extern int libfuse2_readdir(char *path, void *buf, fuse_fill_dir_t filler, off_t, fuse_file_info_t *); +extern int libfuse2_truncate(char *path, off_t off); +extern int libfuse2_rename(char *src, char *dst); +extern int libfuse2_chmod(char *path, mode_t mode); +extern int libfuse2_chown(char *path, uid_t uid, gid_t gid); +extern int libfuse2_utimens(char *path, timespec_t tv[2]); +#else +extern void *libfuse_init(fuse_conn_info_t *conn, fuse_config_t *cfg); +extern int libfuse_getattr(char *path, stat_t *stbuf, fuse_file_info_t *fi); +extern int libfuse_readdir(char *path, void *buf, fuse_fill_dir_t filler, off_t, fuse_file_info_t *, fuse_readdir_flags_t); +extern int libfuse_truncate(char *path, off_t off, fuse_file_info_t *fi); +extern int libfuse_rename(char *src, char *dst, unsigned int flags); +extern int libfuse_chmod(char *path, mode_t mode, fuse_file_info_t *fi); +extern int libfuse_chown(char *path, uid_t uid, gid_t gid, fuse_file_info_t *fi); +extern int libfuse_utimens(char *path, timespec_t tv[2], fuse_file_info_t *fi); +#endif + +// Methods that needs handling in the CGo wrapper for better performance +extern int cloudfuse_cache_update(char *path); +static int native_read_file(char *path, char *buf, size_t size, off_t, fuse_file_info_t *fi); +static int native_write_file(char *path, char *buf, size_t size, off_t, fuse_file_info_t *fi); +static int native_flush_file(char *path, fuse_file_info_t *fi); + +// ------------------------------------------------------------------------------------------------------------- +// Methods not implemented by cloudfuse + +// extern int libfuse_mknod(char *path, mode_t mode, dev_t dev); +// extern int libfuse_link(char *from, char *to); +// extern int libfuse_setxattr(char *path, char *name, char *value, size_t size, int flags); +// extern int libfuse_getxattr(char *path, char *name, char *value, size_t size); +// extern int libfuse_listxattr(char* path, char *list, size_t size); +// extern int libfuse_removexattr(char *path, char *name); +// extern int libfuse_access(char *path, int mask); +// extern int libfuse_lock +// extern int libfuse_bmap +// extern int libfuse_ioctl +// extern int libfuse_poll +// extern int libfuse_write_buf +// extern int libfuse_read_buf +// extern int libfuse_flock +// extern int libfuse_fallocate +// extern int libfuse_copyfilerange +// extern int libfuse_lseek +// ------------------------------------------------------------------------------------------------------------- + +#endif // __LIBFUSE_DEFS_H__ \ No newline at end of file diff --git a/component/libfuse/libfuse_handler.go b/component/libfuse/libfuse_handler.go new file mode 100644 index 000000000..8ea0904e1 --- /dev/null +++ b/component/libfuse/libfuse_handler.go @@ -0,0 +1,1186 @@ +//go:build linux && !fuse2 + +/* + Licensed under the MIT License . + + Copyright © 2023-2024 Seagate Technology LLC and/or its Affiliates + Copyright © 2020-2024 Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE +*/ + +package libfuse + +// CFLAGS: compile time flags -D object file creation. D= Define +// LFLAGS: loader flags link library -l binary file. l=link -ldl is for the extension to dynamically link + +// #cgo CFLAGS: -DFUSE_USE_VERSION=39 -D_FILE_OFFSET_BITS=64 +// #cgo LDFLAGS: -lfuse3 -ldl +// #include "libfuse_wrapper.h" +// #include "extension_handler.h" +import "C" //nolint + +import ( + "errors" + "fmt" + "io" + "io/fs" + "os" + "syscall" + "unsafe" + + "github.com/Seagate/cloudfuse/common" + "github.com/Seagate/cloudfuse/common/log" + "github.com/Seagate/cloudfuse/internal" + "github.com/Seagate/cloudfuse/internal/handlemap" + "github.com/Seagate/cloudfuse/internal/stats_manager" +) + +/* --- IMPORTANT NOTE --- +In below code lot of places we are doing this sort of conversions: + - fi.fh = C.ulong(uintptr(unsafe.Pointer(handle))) + - handle := (*handlemap.Handle)(unsafe.Pointer(uintptr(fi.fh))) + +To open/create calls we need to return back a handle to libfuse which shall be an integer value +We maintain handle as an object, instead of returning back a running integer value as handle +we are converting back the pointer to our handle object to an integer value and sending it to libfuse. +When read/write/flush/close call comes libfuse will supply this handle value back to cloudfuse. +In those calls we will convert integer value back to a pointer and get our valid handle object back for that file. +*/ + +const ( + C_ENOENT = int(-C.ENOENT) + C_EIO = int(-C.EIO) + C_EACCES = int(-C.EACCES) +) + +// Libfuse holds the settings and information for the FUSE component. +type Libfuse struct { + internal.BaseComponent + mountPath string + dirPermission uint + filePermission uint + readOnly bool + attributeExpiration uint32 + entryExpiration uint32 + negativeTimeout uint32 + allowOther bool + allowRoot bool + ownerUID uint32 + ownerGID uint32 + traceEnable bool + extensionPath string + disableWritebackCache bool + ignoreOpenFlags bool + nonEmptyMount bool + networkShare bool // Run as a network file share on Windows + lsFlags common.BitMap16 + maxFuseThreads uint32 + directIO bool + umask uint32 + displayCapacityMb uint64 +} + +type dirChildCache struct { + sIndex uint64 // start index of current block of items + eIndex uint64 // End index of current block of items + length uint64 // Length of the children list + token string // Token to get next block of items from container + children []*internal.ObjAttr // Slice holding current block of children +} + +// Note: libfuse prepends "/" to the path. +// trimFusePath trims the first character from the path provided by libfuse +func trimFusePath(path *C.char) string { + if path == nil { + return "" + } + str := C.GoString(path) + if str != "" { + return str[1:] + } + return str +} + +var fuse_opts C.fuse_options_t // nolint + +// convertConfig converts the config options from Go to C +func (lf *Libfuse) convertConfig() *C.fuse_options_t { + fuse_opts := &C.fuse_options_t{} + + // Note: C strings are allocated in the heap using malloc. Call C.free when string is no longer needed. + fuse_opts.mount_path = C.CString(lf.mountPath) + fuse_opts.uid = C.uid_t(lf.ownerUID) + fuse_opts.gid = C.gid_t(lf.ownerGID) + fuse_opts.permissions = C.uint(lf.filePermission) + fuse_opts.entry_expiry = C.int(lf.entryExpiration) + fuse_opts.attr_expiry = C.int(lf.attributeExpiration) + fuse_opts.negative_expiry = C.int(lf.negativeTimeout) + fuse_opts.readonly = C.bool(lf.readOnly) + fuse_opts.allow_other = C.bool(lf.allowOther) + fuse_opts.allow_root = C.bool(lf.allowRoot) + fuse_opts.trace_enable = C.bool(lf.traceEnable) + fuse_opts.umask = C.int(lf.umask) + + return fuse_opts +} + +// initFuse initializes the fuse library by registering callbacks, parsing arguments and mounting the directory +func (lf *Libfuse) initFuse() error { + log.Trace("Libfuse::initFuse : Initializing FUSE3") + + operations := C.fuse_operations_t{} + + if lf.extensionPath != "" { + log.Trace("Libfuse::InitFuse : Going for extension mouting [%s]", lf.extensionPath) + + // User has given an extension so we need to register it to fuse + // and then register ourself to it + extensionPath := C.CString(lf.extensionPath) + defer C.free(unsafe.Pointer(extensionPath)) + + // Load the library + errc := C.load_library(extensionPath) + if errc != 0 { + log.Err("Libfuse::InitFuse : Failed to load extension err code %d", errc) + return errors.New("failed to load extension") + } + log.Trace("Libfuse::InitFuse : Extension loaded") + + // Get extension callback table + errc = C.get_extension_callbacks(&operations) + if errc != 0 { + C.unload_library() + log.Err("Libfuse::InitFuse : Failed to get callback table from extension. error code %d", errc) + return errors.New("failed to get callback table from extension") + } + log.Trace("Libfuse::InitFuse : Extension callback retrieved") + + // Get our callback table + my_operations := C.fuse_operations_t{} + C.populate_callbacks(&my_operations) + + // Send our callback table to the extension + errc = C.register_callback_to_extension(&my_operations) + if errc != 0 { + C.unload_library() + log.Err("Libfuse::InitFuse : Failed to register callback table to extension. error code %d", errc) + return errors.New("failed to register callback table to extension") + } + log.Trace("Libfuse::InitFuse : Callbacks registered to extension") + } else { + // Populate our methods to be registered to libfuse + log.Trace("Libfuse::initFuse : Registering fuse callbacks") + C.populate_callbacks(&operations) + } + + log.Trace("Libfuse::initFuse : Populating fuse arguments") + fuse_opts := lf.convertConfig() + var args C.fuse_args_t + + fuse_opts, ret := populateFuseArgs(fuse_opts, &args) + if ret != 0 { + log.Err("Libfuse::initFuse : Failed to parse fuse arguments") + return errors.New("failed to parse fuse arguments") + } + // Note: C strings are allocated in the heap using malloc. Calling C.free to release the mount path since it is no longer needed. + C.free(unsafe.Pointer(fuse_opts.mount_path)) + + log.Info("Libfuse::initFuse : Mounting with fuse3 library") + ret = C.start_fuse(&args, &operations) + if ret != 0 { + log.Err("Libfuse::initFuse : failed to mount fuse") + return errors.New("failed to mount fuse") + } + + return nil +} + +// populateFuseArgs populates libfuse args before we call start_fuse +func populateFuseArgs(opts *C.fuse_options_t, args *C.fuse_args_t) (*C.fuse_options_t, C.int) { + log.Trace("Libfuse::populateFuseArgs") + if args == nil { + return nil, 1 + } + args.argc = 0 + args.allocated = 1 + + arguments := make([]string, 0) + options := fmt.Sprintf("entry_timeout=%d,attr_timeout=%d,negative_timeout=%d", + opts.entry_expiry, + opts.attr_expiry, + opts.negative_expiry) + + if opts.allow_other { + options += ",allow_other" + } + + if opts.allow_root { + options += ",allow_root" + } + + if opts.readonly { + options += ",ro" + } + + if opts.umask != 0 { + options += fmt.Sprintf(",umask=%04d", opts.umask) + } + + options += ",max_read=1048576" + + if !fuseFS.directIO { + options += ",kernel_cache" + } + + // Why we pass -f + // CGo is not very good with handling forks - so if the user wants to run cloudfuse in the + // background we fork on mount in GO (mount.go) and we just always force libfuse to mount in foreground + arguments = append(arguments, "cloudfuse", + C.GoString(opts.mount_path), + "-o", options, + "-f", "-ofsname=cloudfuse") // "-omax_read=4194304" + + if opts.trace_enable { + arguments = append(arguments, "-d") + } + + for _, a := range arguments { + log.Debug("Libfuse::populateFuseArgs : opts : %s", a) + arg := C.CString(a) + defer C.free(unsafe.Pointer(arg)) + err := C.fuse_opt_add_arg(args, arg) + if err != 0 { + return nil, err + } + } + + return opts, 0 +} + +// destroyFuse is a no-op +func (lf *Libfuse) destroyFuse() error { + log.Trace("Libfuse::destroyFuse : Destroying FUSE") + return nil +} + +//export libfuse_init +func libfuse_init(conn *C.fuse_conn_info_t, cfg *C.fuse_config_t) (res unsafe.Pointer) { + log.Trace("Libfuse::libfuse_init : init (read : %v, write %v, read-ahead %v)", conn.max_read, conn.max_write, conn.max_readahead) + + log.Info("Libfuse::NotifyMountToParent : Notifying parent for successful mount") + if err := common.NotifyMountToParent(); err != nil { + log.Err("Libfuse::NotifyMountToParent : Failed to notify parent, error: [%v]", err) + } + + C.populate_uid_gid() + + log.Info("Libfuse::libfuse_init : Kernel Caps : %d", conn.capable) + + // Populate connection information + // conn.want |= C.FUSE_CAP_NO_OPENDIR_SUPPORT + + // Allow fuse to perform parallel operations on a directory + if (conn.capable & C.FUSE_CAP_PARALLEL_DIROPS) != 0 { + log.Info("Libfuse::libfuse_init : Enable Capability : FUSE_CAP_PARALLEL_DIROPS") + conn.want |= C.FUSE_CAP_PARALLEL_DIROPS + } + + // Kernel shall invalidate the data in page cache if file size of LMT changes + if (conn.capable & C.FUSE_CAP_AUTO_INVAL_DATA) != 0 { + log.Info("Libfuse::libfuse_init : Enable Capability : FUSE_CAP_AUTO_INVAL_DATA") + conn.want |= C.FUSE_CAP_AUTO_INVAL_DATA + } + + // Enable read-dir plus where attributes of each file are returned back + // in the list call itself and fuse does not need to fire getAttr after list + if (conn.capable & C.FUSE_CAP_READDIRPLUS) != 0 { + log.Info("Libfuse::libfuse_init : Enable Capability : FUSE_CAP_READDIRPLUS") + conn.want |= C.FUSE_CAP_READDIRPLUS + } + + // Allow fuse to read a file in parallel on different offsets + if (conn.capable & C.FUSE_CAP_ASYNC_READ) != 0 { + log.Info("Libfuse::libfuse_init : Enable Capability : FUSE_CAP_ASYNC_READ") + conn.want |= C.FUSE_CAP_ASYNC_READ + } + + if (conn.capable & C.FUSE_CAP_SPLICE_WRITE) != 0 { + // While writing to fuse device let libfuse collate the data and write big chunks + log.Info("Libfuse::libfuse_init : Enable Capability : FUSE_CAP_SPLICE_WRITE") + conn.want |= C.FUSE_CAP_SPLICE_WRITE + } + + /* + FUSE_CAP_WRITEBACK_CACHE flag is not suitable for network filesystems. If a partial page is + written, then the page needs to be first read from userspace. This means, that + even for files opened for O_WRONLY it is possible that READ requests will be + generated by the kernel. + */ + if (!fuseFS.directIO) && (!fuseFS.disableWritebackCache) && ((conn.capable & C.FUSE_CAP_WRITEBACK_CACHE) != 0) { + // Buffer write requests at libfuse and then hand it off to application + log.Info("Libfuse::libfuse_init : Enable Capability : FUSE_CAP_WRITEBACK_CACHE") + conn.want |= C.FUSE_CAP_WRITEBACK_CACHE + } + + // Max background thread on the fuse layer for high parallelism + conn.max_background = C.uint(fuseFS.maxFuseThreads) + + // While reading a file let kernel do readahed for better perf + conn.max_readahead = (4 * 1024 * 1024) + conn.max_read = (1 * 1024 * 1024) + + // RHEL still has 3.3 fuse version and it does not allow max_write beyond 128K + // Setting this value to 1 MB will fail the mount. + fuse_minor := common.GetFuseMinorVersion() + if fuse_minor > 4 { + log.Info("Libfuse::libfuse_init : Setting 1MB max_write for fuse minor %v", fuse_minor) + conn.max_write = (1 * 1024 * 1024) + } else { + log.Info("Libfuse::libfuse_init : Ignoring max_write for fuse minor %v", fuse_minor) + conn.max_write = (128 * 1024) + } + + // direct_io option is used to bypass the kernel cache. It disables the use of + // page cache (file content cache) in the kernel for the filesystem. + if fuseFS.directIO { + cfg.direct_io = C.int(1) + } + + return nil +} + +//export libfuse_destroy +func libfuse_destroy(data unsafe.Pointer) { + log.Trace("Libfuse::libfuse_destroy : destroy") +} + +func (lf *Libfuse) fillStat(attr *internal.ObjAttr, stbuf *C.stat_t) { + (*stbuf).st_uid = C.uint(lf.ownerUID) + (*stbuf).st_gid = C.uint(lf.ownerGID) + (*stbuf).st_nlink = 1 + (*stbuf).st_size = C.long(attr.Size) + + // Populate mode + // Backing storage implementation has support for mode. + if !attr.IsModeDefault() { + (*stbuf).st_mode = C.uint(attr.Mode) & 0xffffffff + } else { + if attr.IsDir() { + (*stbuf).st_mode = C.uint(lf.dirPermission) & 0xffffffff + } else { + (*stbuf).st_mode = C.uint(lf.filePermission) & 0xffffffff + } + } + + if attr.IsDir() { + (*stbuf).st_nlink = 2 + (*stbuf).st_size = 4096 + (*stbuf).st_mode |= C.S_IFDIR + } else if attr.IsSymlink() { + (*stbuf).st_mode |= C.S_IFLNK + } else { + (*stbuf).st_mode |= C.S_IFREG + } + + (*stbuf).st_atim.tv_sec = C.long(attr.Atime.Unix()) + (*stbuf).st_atim.tv_nsec = 0 + + (*stbuf).st_ctim.tv_sec = C.long(attr.Ctime.Unix()) + (*stbuf).st_ctim.tv_nsec = 0 + + (*stbuf).st_mtim.tv_sec = C.long(attr.Mtime.Unix()) + (*stbuf).st_mtim.tv_nsec = 0 +} + +// File System Operations +// Similar to well known UNIX file system operations +// Instead of returning an error in 'errno', return the negated error value (-errno) directly. +// Kernel will perform permission checking if `default_permissions` mount option was passed to `fuse_main()` +// otherwise, perform necessary permission checking + +// libfuse_getattr gets file attributes +// +//export libfuse_getattr +func libfuse_getattr(path *C.char, stbuf *C.stat_t, fi *C.fuse_file_info_t) C.int { + name := trimFusePath(path) + name = common.NormalizeObjectName(name) + // log.Trace("Libfuse::libfuse_getattr : %s", name) + + // Return the default configuration for the root + if name == "" { + return C.get_root_properties(stbuf) + } + + // TODO: How does this work if we trim the path? + // Check if the file is meant to be ignored + if ignore, found := ignoreFiles[name]; found && ignore { + return -C.ENOENT + } + + // Get attributes + attr, err := fuseFS.NextComponent().GetAttr(internal.GetAttrOptions{Name: name}) + if err != nil { + // log.Err("Libfuse::libfuse_getattr : Failed to get attributes of %s [%s]", name, err.Error()) + if err == syscall.ENOENT { + return -C.ENOENT + } else if err == syscall.EACCES { + return -C.EACCES + } else { + return -C.EIO + } + } + + // Populate stat + fuseFS.fillStat(attr, stbuf) + return 0 +} + +// Directory Operations + +// libfuse_mkdir creates a directory +// +//export libfuse_mkdir +func libfuse_mkdir(path *C.char, mode C.mode_t) C.int { + name := trimFusePath(path) + name = common.NormalizeObjectName(name) + log.Trace("Libfuse::libfuse_mkdir : %s", name) + + err := fuseFS.NextComponent().CreateDir(internal.CreateDirOptions{Name: name, Mode: fs.FileMode(uint32(mode) & 0xffffffff)}) + if err != nil { + log.Err("Libfuse::libfuse_mkdir : Failed to create %s [%s]", name, err.Error()) + if os.IsPermission(err) { + return -C.EACCES + } else if os.IsExist(err) { + return -C.EEXIST + } else { + return -C.EIO + } + } + + libfuseStatsCollector.PushEvents(createDir, name, map[string]interface{}{md: fs.FileMode(uint32(mode) & 0xffffffff)}) + libfuseStatsCollector.UpdateStats(stats_manager.Increment, createDir, (int64)(1)) + + return 0 +} + +// libfuse_opendir opens handle to given directory +// +//export libfuse_opendir +func libfuse_opendir(path *C.char, fi *C.fuse_file_info_t) C.int { + name := trimFusePath(path) + name = common.NormalizeObjectName(name) + if name != "" { + name = name + "/" + } + + log.Trace("Libfuse::libfuse_opendir : %s", name) + + handle := handlemap.NewHandle(name) + + // For each handle created using opendir we create + // this structure here to hold current block of children to serve readdir + handle.SetValue("cache", &dirChildCache{ + sIndex: 0, + eIndex: 0, + token: "", + length: 0, + children: make([]*internal.ObjAttr, 0), + }) + + handlemap.Add(handle) + fi.fh = C.ulong(uintptr(unsafe.Pointer(handle))) + + return 0 +} + +// libfuse_releasedir opens handle to given directory +// +//export libfuse_releasedir +func libfuse_releasedir(path *C.char, fi *C.fuse_file_info_t) C.int { + handle := (*handlemap.Handle)(unsafe.Pointer(uintptr(fi.fh))) + + log.Trace("Libfuse::libfuse_releasedir : %s, handle: %d", handle.Path, handle.ID) + + handle.Cleanup() + handlemap.Delete(handle.ID) + return 0 +} + +// libfuse_readdir reads a directory +// +//export libfuse_readdir +func libfuse_readdir(_ *C.char, buf unsafe.Pointer, filler C.fuse_fill_dir_t, off C.off_t, fi *C.fuse_file_info_t, flag C.fuse_readdir_flags_t) C.int { + handle := (*handlemap.Handle)(unsafe.Pointer(uintptr(fi.fh))) + + handle.RLock() + val, found := handle.GetValue("cache") + handle.RUnlock() + + if !found { + return C.int(C_EIO) + } + + off_64 := uint64(off) + cacheInfo := val.(*dirChildCache) + if off_64 == 0 || + (off_64 >= cacheInfo.eIndex && cacheInfo.token != "") { + attrs, token, err := fuseFS.NextComponent().StreamDir(internal.StreamDirOptions{ + Name: handle.Path, + Offset: off_64, + Token: cacheInfo.token, + Count: common.MaxDirListCount, + }) + + if err != nil { + log.Err("Libfuse::libfuse_readdir : Path %s, handle: %d, offset %d. Error in retrieval %s", handle.Path, handle.ID, off_64, err.Error()) + if os.IsNotExist(err) { + return C.int(C_ENOENT) + } else if os.IsPermission(err) { + return C.int(C_EACCES) + } else { + return C.int(C_EIO) + } + } + + // TODO: Investigate why this works in fuse2 but not fuse3 + // if off_64 == 0 { + // attrs = append([]*internal.ObjAttr{{Flags: fuseFS.lsFlags, Name: "."}, {Flags: fuseFS.lsFlags, Name: ".."}}, attrs...) + // } + + cacheInfo.sIndex = off_64 + cacheInfo.eIndex = off_64 + uint64(len(attrs)) + cacheInfo.length = uint64(len(attrs)) + cacheInfo.token = token + cacheInfo.children = cacheInfo.children[:0] + cacheInfo.children = attrs + } + + if off_64 >= cacheInfo.eIndex { + // If offset is still beyond the end index limit then we are done iterating + return 0 + } + + stbuf := C.stat_t{} + idx := C.long(off) + + // Populate the stat by calling filler + for segmentIdx := off_64 - cacheInfo.sIndex; segmentIdx < cacheInfo.length; segmentIdx++ { + fuseFS.fillStat(cacheInfo.children[segmentIdx], &stbuf) + + name := C.CString(cacheInfo.children[segmentIdx].Name) + if 0 != C.fill_dir_entry(filler, buf, name, &stbuf, idx+1) { + C.free(unsafe.Pointer(name)) + break + } + + C.free(unsafe.Pointer(name)) + idx++ + } + + return 0 +} + +// libfuse_rmdir deletes a directory, which must be empty. +// +//export libfuse_rmdir +func libfuse_rmdir(path *C.char) C.int { + name := trimFusePath(path) + name = common.NormalizeObjectName(name) + log.Trace("Libfuse::libfuse_rmdir : %s", name) + + empty := fuseFS.NextComponent().IsDirEmpty(internal.IsDirEmptyOptions{Name: name}) + if !empty { + return -C.ENOTEMPTY + } + + err := fuseFS.NextComponent().DeleteDir(internal.DeleteDirOptions{Name: name}) + if err != nil { + log.Err("Libfuse::libfuse_rmdir : Failed to delete %s [%s]", name, err.Error()) + if os.IsNotExist(err) { + return -C.ENOENT + } else { + return -C.EIO + } + } + + libfuseStatsCollector.PushEvents(deleteDir, name, nil) + libfuseStatsCollector.UpdateStats(stats_manager.Increment, deleteDir, (int64)(1)) + + return 0 +} + +// File Operations +// +//export libfuse_statfs +func libfuse_statfs(path *C.char, buf *C.statvfs_t) C.int { + name := trimFusePath(path) + name = common.NormalizeObjectName(name) + log.Trace("Libfuse::libfuse_statfs : %s", name) + + attr, populated, err := fuseFS.NextComponent().StatFs() + if err != nil { + log.Err("Libfuse::libfuse_statfs : Failed to get stats %s [%s]", name, err.Error()) + return -C.EIO + } + + // if populated then we need to overwrite root attributes + if populated { + (*buf).f_bsize = C.ulong(attr.Bsize) + (*buf).f_frsize = C.ulong(attr.Frsize) + (*buf).f_blocks = C.ulong(attr.Blocks) + (*buf).f_bavail = C.ulong(attr.Bavail) + (*buf).f_bfree = C.ulong(attr.Bfree) + (*buf).f_files = C.ulong(attr.Files) + (*buf).f_ffree = C.ulong(attr.Ffree) + (*buf).f_flag = C.ulong(attr.Flags) + return 0 + } + + C.populate_statfs(path, buf) + + return 0 +} + +// libfuse_create creates a file with the specified mode and then opens it. +// +//export libfuse_create +func libfuse_create(path *C.char, mode C.mode_t, fi *C.fuse_file_info_t) C.int { + name := trimFusePath(path) + name = common.NormalizeObjectName(name) + log.Trace("Libfuse::libfuse_create : %s", name) + + handle, err := fuseFS.NextComponent().CreateFile(internal.CreateFileOptions{Name: name, Mode: fs.FileMode(uint32(mode) & 0xffffffff)}) + if err != nil { + log.Err("Libfuse::libfuse_create : Failed to create %s [%s]", name, err.Error()) + if os.IsExist(err) { + return -C.EEXIST + } else if os.IsPermission(err) { + return -C.EACCES + } else { + return -C.EIO + } + } + + handlemap.Add(handle) + ret_val := C.allocate_native_file_object(0, C.ulong(uintptr(unsafe.Pointer(handle))), 0) + if !handle.Cached() { + ret_val.fd = 0 + } + + log.Trace("Libfuse::libfuse_create : %s, handle %d", name, handle.ID) + fi.fh = C.ulong(uintptr(unsafe.Pointer(ret_val))) + + libfuseStatsCollector.PushEvents(createFile, name, map[string]interface{}{md: fs.FileMode(uint32(mode) & 0xffffffff)}) + + // increment open file handles count + libfuseStatsCollector.UpdateStats(stats_manager.Increment, openHandles, (int64)(1)) + + return 0 +} + +// libfuse_open opens a file +// +//export libfuse_open +func libfuse_open(path *C.char, fi *C.fuse_file_info_t) C.int { + name := trimFusePath(path) + name = common.NormalizeObjectName(name) + log.Trace("Libfuse::libfuse_open : %s", name) + // TODO: Should this sit behind a user option? What if we change something to support these in the future? + // Mask out SYNC and DIRECT flags since write operation will fail + if fi.flags&C.O_SYNC != 0 || fi.flags&C.__O_DIRECT != 0 { + log.Info("Libfuse::libfuse_open : Reset flags for open %s, fi.flags %X", name, fi.flags) + // Cloudfuse does not support the SYNC or DIRECT flag. If a user application passes this flag on to cloudfuse + // and we open the file with this flag, subsequent write operations will fail with "Invalid argument" error. + // Mask them out here in the open call so that write works. + // Oracle RMAN is one such application that sends these flags during backup + fi.flags = fi.flags &^ C.O_SYNC + fi.flags = fi.flags &^ C.__O_DIRECT + } + if !fuseFS.disableWritebackCache { + if fi.flags&C.O_ACCMODE == C.O_WRONLY || fi.flags&C.O_APPEND != 0 { + if fuseFS.ignoreOpenFlags { + log.Warn("Libfuse::libfuse_open : Flags (%X) not supported to open %s when write back cache is on. Ignoring unsupported flags.", fi.flags, name) + // O_ACCMODE disables both RDONLY, WRONLY and RDWR flags + fi.flags = fi.flags &^ (C.O_APPEND | C.O_ACCMODE) + fi.flags = fi.flags | C.O_RDWR + } else { + log.Err("Libfuse::libfuse_open : Flag (%X) not supported to open %s when write back cache is on. Pass --disable-writeback-cache=true or --ignore-open-flags=true via CLI", fi.flags, name) + return -C.EINVAL + } + } + } + + handle, err := fuseFS.NextComponent().OpenFile( + internal.OpenFileOptions{ + Name: name, + Flags: int(int(fi.flags) & 0xffffffff), + Mode: fs.FileMode(fuseFS.filePermission), + }) + + if err != nil { + log.Err("Libfuse::libfuse_open : Failed to open %s [%s]", name, err.Error()) + if os.IsNotExist(err) { + return -C.ENOENT + } else if os.IsPermission(err) { + return -C.EACCES + } else { + return -C.EIO + } + } + + handlemap.Add(handle) + //fi.fh = C.ulong(uintptr(unsafe.Pointer(handle))) + ret_val := C.allocate_native_file_object(C.ulong(handle.UnixFD), C.ulong(uintptr(unsafe.Pointer(handle))), C.ulong(handle.Size)) + if !handle.Cached() { + ret_val.fd = 0 + } + log.Trace("Libfuse::libfuse_open : %s, handle %d", name, handle.ID) + fi.fh = C.ulong(uintptr(unsafe.Pointer(ret_val))) + + // increment open file handles count + libfuseStatsCollector.UpdateStats(stats_manager.Increment, openHandles, (int64)(1)) + + return 0 +} + +// libfuse_read reads data from an open file +// +//export libfuse_read +func libfuse_read(path *C.char, buf *C.char, size C.size_t, off C.off_t, fi *C.fuse_file_info_t) C.int { + fileHandle := (*C.file_handle_t)(unsafe.Pointer(uintptr(fi.fh))) + handle := (*handlemap.Handle)(unsafe.Pointer(uintptr(fileHandle.obj))) + + offset := uint64(off) + data := (*[1 << 30]byte)(unsafe.Pointer(buf)) + + var err error + var bytesRead int + + if handle.Cached() { + bytesRead, err = syscall.Pread(handle.FD(), data[:size], int64(offset)) + //bytesRead, err = handle.FObj.ReadAt(data[:size], int64(offset)) + } else { + bytesRead, err = fuseFS.NextComponent().ReadInBuffer( + internal.ReadInBufferOptions{ + Handle: handle, + Offset: int64(offset), + Data: data[:size], + }) + } + + if err == io.EOF { + err = nil + } + if err != nil { + log.Err("Libfuse::libfuse_read : error reading file %s, handle: %d [%s]", handle.Path, handle.ID, err.Error()) + return -C.EIO + } + + return C.int(bytesRead) +} + +// libfuse_write writes data to an open file +// +//export libfuse_write +func libfuse_write(path *C.char, buf *C.char, size C.size_t, off C.off_t, fi *C.fuse_file_info_t) C.int { + fileHandle := (*C.file_handle_t)(unsafe.Pointer(uintptr(fi.fh))) + handle := (*handlemap.Handle)(unsafe.Pointer(uintptr(fileHandle.obj))) + + offset := uint64(off) + data := (*[1 << 30]byte)(unsafe.Pointer(buf)) + // log.Debug("Libfuse::libfuse_write : Offset %v, Data %v", offset, size) + bytesWritten, err := fuseFS.NextComponent().WriteFile( + internal.WriteFileOptions{ + Handle: handle, + Offset: int64(offset), + Data: data[:size], + Metadata: nil, + }) + + if err != nil { + log.Err("Libfuse::libfuse_write : error writing file %s, handle: %d [%s]", handle.Path, handle.ID, err.Error()) + return -C.EIO + } + + return C.int(bytesWritten) +} + +// libfuse_flush possibly flushes cached data +// +//export libfuse_flush +func libfuse_flush(path *C.char, fi *C.fuse_file_info_t) C.int { + fileHandle := (*C.file_handle_t)(unsafe.Pointer(uintptr(fi.fh))) + handle := (*handlemap.Handle)(unsafe.Pointer(uintptr(fileHandle.obj))) + log.Trace("Libfuse::libfuse_flush : %s, handle: %d", handle.Path, handle.ID) + + // If the file handle is not dirty, there is no need to flush + if fileHandle.dirty != 0 { + handle.Flags.Set(handlemap.HandleFlagDirty) + } + + if !handle.Dirty() { + return 0 + } + + err := fuseFS.NextComponent().FlushFile(internal.FlushFileOptions{Handle: handle}) + if err != nil { + log.Err("Libfuse::libfuse_flush : error flushing file %s, handle: %d [%s]", handle.Path, handle.ID, err.Error()) + if err == syscall.ENOENT { + return -C.ENOENT + } else if err == syscall.EACCES { + return -C.EACCES + } else { + return -C.EIO + } + } + + return 0 +} + +// libfuse_truncate changes the size of a file +// +//export libfuse_truncate +func libfuse_truncate(path *C.char, off C.off_t, fi *C.fuse_file_info_t) C.int { + name := trimFusePath(path) + name = common.NormalizeObjectName(name) + log.Trace("Libfuse::libfuse_truncate : %s size %d", name, off) + + err := fuseFS.NextComponent().TruncateFile(internal.TruncateFileOptions{Name: name, Size: int64(off)}) + if err != nil { + log.Err("Libfuse::libfuse_truncate : error truncating file %s [%s]", name, err.Error()) + if os.IsNotExist(err) { + return -C.ENOENT + } + return -C.EIO + } + + libfuseStatsCollector.PushEvents(truncateFile, name, map[string]interface{}{size: int64(off)}) + libfuseStatsCollector.UpdateStats(stats_manager.Increment, truncateFile, (int64)(1)) + + return 0 +} + +// libfuse_release releases an open file +// +//export libfuse_release +func libfuse_release(path *C.char, fi *C.fuse_file_info_t) C.int { + fileHandle := (*C.file_handle_t)(unsafe.Pointer(uintptr(fi.fh))) + handle := (*handlemap.Handle)(unsafe.Pointer(uintptr(fileHandle.obj))) + + log.Trace("Libfuse::libfuse_release : %s, handle: %d", handle.Path, handle.ID) + + // If the file handle is dirty then file-cache needs to flush this file + if fileHandle.dirty != 0 { + handle.Flags.Set(handlemap.HandleFlagDirty) + } + + err := fuseFS.NextComponent().CloseFile(internal.CloseFileOptions{Handle: handle}) + if err != nil { + log.Err("Libfuse::libfuse_release : error closing file %s, handle: %d [%s]", handle.Path, handle.ID, err.Error()) + if err == syscall.ENOENT { + return -C.ENOENT + } else if err == syscall.EACCES { + return -C.EACCES + } else { + return -C.EIO + } + } + + handlemap.Delete(handle.ID) + C.release_native_file_object(fi) + + // decrement open file handles count + libfuseStatsCollector.UpdateStats(stats_manager.Decrement, openHandles, (int64)(1)) + + return 0 +} + +// libfuse_unlink removes a file +// +//export libfuse_unlink +func libfuse_unlink(path *C.char) C.int { + name := trimFusePath(path) + name = common.NormalizeObjectName(name) + log.Trace("Libfuse::libfuse_unlink : %s", name) + + err := fuseFS.NextComponent().DeleteFile(internal.DeleteFileOptions{Name: name}) + if err != nil { + log.Err("Libfuse::libfuse_unlink : error deleting file %s [%s]", name, err.Error()) + if os.IsNotExist(err) { + return -C.ENOENT + } else if os.IsPermission(err) { + return -C.EACCES + } + return -C.EIO + } + + libfuseStatsCollector.PushEvents(deleteFile, name, nil) + libfuseStatsCollector.UpdateStats(stats_manager.Increment, deleteFile, (int64)(1)) + + return 0 +} + +// libfuse_rename renames a file or directory +// https://man7.org/linux/man-pages/man2/rename.2.html +// errors handled: EISDIR, ENOENT, ENOTDIR, ENOTEMPTY, EEXIST +// TODO: handle EACCESS, EINVAL? +// +//export libfuse_rename +func libfuse_rename(src *C.char, dst *C.char, flags C.uint) C.int { + srcPath := trimFusePath(src) + srcPath = common.NormalizeObjectName(srcPath) + dstPath := trimFusePath(dst) + dstPath = common.NormalizeObjectName(dstPath) + log.Trace("Libfuse::libfuse_rename : %s -> %s", srcPath, dstPath) + // Note: When running other commands from the command line, a lot of them seemed to handle some cases like ENOENT themselves. + // Rename did not, so we manually check here. + + // TODO: Support for RENAME_EXCHANGE + if flags&C.RENAME_EXCHANGE != 0 { + return -C.ENOTSUP + } + + // ENOENT. Not covered: a directory component in dst does not exist + if srcPath == "" || dstPath == "" { + log.Err("Libfuse::libfuse_rename : src: [%s] or dst: [%s] is an empty string", srcPath, dstPath) + return -C.ENOENT + } + + srcAttr, srcErr := fuseFS.NextComponent().GetAttr(internal.GetAttrOptions{Name: srcPath}) + if os.IsNotExist(srcErr) { + log.Err("Libfuse::libfuse_rename : Failed to get attributes of %s [%s]", srcPath, srcErr.Error()) + return -C.ENOENT + } + dstAttr, dstErr := fuseFS.NextComponent().GetAttr(internal.GetAttrOptions{Name: dstPath}) + + // EEXIST + if flags&C.RENAME_NOREPLACE != 0 && (dstErr == nil || os.IsExist(dstErr)) { + return -C.EEXIST + } + + // EISDIR + if (dstErr == nil || os.IsExist(dstErr)) && dstAttr.IsDir() && !srcAttr.IsDir() { + log.Err("Libfuse::libfuse_rename : dst [%s] is an existing directory but src [%s] is not a directory", dstPath, srcPath) + return -C.EISDIR + } + + // ENOTDIR + if (dstErr == nil || os.IsExist(dstErr)) && !dstAttr.IsDir() && srcAttr.IsDir() { + log.Err("Libfuse::libfuse_rename : dst [%s] is an existing file but src [%s] is a directory", dstPath, srcPath) + return -C.ENOTDIR + } + + if srcAttr.IsDir() { + // ENOTEMPTY + if dstErr == nil || os.IsExist(dstErr) { + empty := fuseFS.NextComponent().IsDirEmpty(internal.IsDirEmptyOptions{Name: dstPath}) + if !empty { + return -C.ENOTEMPTY + } + } + + err := fuseFS.NextComponent().RenameDir(internal.RenameDirOptions{Src: srcPath, Dst: dstPath}) + if err != nil { + log.Err("Libfuse::libfuse_rename : error renaming directory %s -> %s [%s]", srcPath, dstPath, err.Error()) + return -C.EIO + } + + libfuseStatsCollector.PushEvents(renameDir, srcPath, map[string]interface{}{source: srcPath, dest: dstPath}) + libfuseStatsCollector.UpdateStats(stats_manager.Increment, renameDir, (int64)(1)) + + } else { + err := fuseFS.NextComponent().RenameFile(internal.RenameFileOptions{Src: srcPath, Dst: dstPath}) + if err != nil { + log.Err("Libfuse::libfuse_rename : error renaming file %s -> %s [%s]", srcPath, dstPath, err.Error()) + return -C.EIO + } + + libfuseStatsCollector.PushEvents(renameFile, srcPath, map[string]interface{}{source: srcPath, dest: dstPath}) + libfuseStatsCollector.UpdateStats(stats_manager.Increment, renameFile, (int64)(1)) + + } + + return 0 +} + +// Symlink Operations + +// libfuse_symlink creates a symbolic link +// +//export libfuse_symlink +func libfuse_symlink(target *C.char, link *C.char) C.int { + name := trimFusePath(link) + name = common.NormalizeObjectName(name) + targetPath := C.GoString(target) + targetPath = common.NormalizeObjectName(targetPath) + log.Trace("Libfuse::libfuse_symlink : Received for %s -> %s", name, targetPath) + + err := fuseFS.NextComponent().CreateLink(internal.CreateLinkOptions{Name: name, Target: targetPath}) + if err != nil { + log.Err("Libfuse::libfuse_symlink : error linking file %s -> %s [%s]", name, targetPath, err.Error()) + return -C.EIO + } + + libfuseStatsCollector.PushEvents(createLink, name, map[string]interface{}{trgt: targetPath}) + libfuseStatsCollector.UpdateStats(stats_manager.Increment, createLink, (int64)(1)) + + return 0 +} + +// libfuse_readlink reads the target of a symbolic link +// +//export libfuse_readlink +func libfuse_readlink(path *C.char, buf *C.char, size C.size_t) C.int { + name := trimFusePath(path) + name = common.NormalizeObjectName(name) + //log.Trace("Libfuse::libfuse_readlink : Received for %s", name) + + targetPath, err := fuseFS.NextComponent().ReadLink(internal.ReadLinkOptions{Name: name}) + if err != nil { + log.Err("Libfuse::libfuse_readlink : error reading link file %s [%s]", name, err.Error()) + if os.IsNotExist(err) { + return -C.ENOENT + } + return -C.EIO + } + data := (*[1 << 30]byte)(unsafe.Pointer(buf)) + copy(data[:size-1], targetPath) + data[len(targetPath)] = 0 + + libfuseStatsCollector.PushEvents(readLink, name, map[string]interface{}{trgt: targetPath}) + libfuseStatsCollector.UpdateStats(stats_manager.Increment, readLink, (int64)(1)) + + return 0 +} + +// libfuse_fsync synchronizes file contents +// +//export libfuse_fsync +func libfuse_fsync(path *C.char, datasync C.int, fi *C.fuse_file_info_t) C.int { + if fi.fh == 0 { + return C.int(-C.EIO) + } + + fileHandle := (*C.file_handle_t)(unsafe.Pointer(uintptr(fi.fh))) + handle := (*handlemap.Handle)(unsafe.Pointer(uintptr(fileHandle.obj))) + log.Trace("Libfuse::libfuse_fsync : %s, handle: %d", handle.Path, handle.ID) + + options := internal.SyncFileOptions{Handle: handle} + // If the datasync parameter is non-zero, then only the user data should be flushed, not the metadata. + // TODO : Should we support this? + + err := fuseFS.NextComponent().SyncFile(options) + if err != nil { + log.Err("Libfuse::libfuse_fsync : error syncing file %s [%s]", handle.Path, err.Error()) + return -C.EIO + } + + libfuseStatsCollector.PushEvents(syncFile, handle.Path, nil) + libfuseStatsCollector.UpdateStats(stats_manager.Increment, syncFile, (int64)(1)) + + return 0 +} + +// libfuse_fsyncdir synchronizes directory contents +// +//export libfuse_fsyncdir +func libfuse_fsyncdir(path *C.char, datasync C.int, fi *C.fuse_file_info_t) C.int { + name := trimFusePath(path) + name = common.NormalizeObjectName(name) + log.Trace("Libfuse::libfuse_fsyncdir : %s", name) + + options := internal.SyncDirOptions{Name: name} + // If the datasync parameter is non-zero, then only the user data should be flushed, not the metadata. + // TODO : Should we support this? + + err := fuseFS.NextComponent().SyncDir(options) + if err != nil { + log.Err("Libfuse::libfuse_fsyncdir : error syncing dir %s [%s]", name, err.Error()) + return -C.EIO + } + + libfuseStatsCollector.PushEvents(syncDir, name, nil) + libfuseStatsCollector.UpdateStats(stats_manager.Increment, syncDir, (int64)(1)) + + return 0 +} + +// libfuse_chmod changes permission bits of a file +// +//export libfuse_chmod +func libfuse_chmod(path *C.char, mode C.mode_t, fi *C.fuse_file_info_t) C.int { + name := trimFusePath(path) + name = common.NormalizeObjectName(name) + log.Trace("Libfuse::libfuse_chmod : %s", name) + + err := fuseFS.NextComponent().Chmod( + internal.ChmodOptions{ + Name: name, + Mode: fs.FileMode(uint32(mode) & 0xffffffff), + }) + if err != nil { + log.Err("Libfuse::libfuse_chmod : error in chmod of %s [%s]", name, err.Error()) + if os.IsNotExist(err) { + return -C.ENOENT + } else if os.IsPermission(err) { + return -C.EACCES + } + return -C.EIO + } + + libfuseStatsCollector.PushEvents(chmod, name, map[string]interface{}{md: fs.FileMode(uint32(mode) & 0xffffffff)}) + libfuseStatsCollector.UpdateStats(stats_manager.Increment, chmod, (int64)(1)) + + return 0 +} + +// libfuse_chown changes the owner and group of a file +// +//export libfuse_chown +func libfuse_chown(path *C.char, uid C.uid_t, gid C.gid_t, fi *C.fuse_file_info_t) C.int { + name := trimFusePath(path) + name = common.NormalizeObjectName(name) + log.Trace("Libfuse::libfuse_chown : %s", name) + // TODO: Implement + return 0 +} + +// libfuse_utimens changes the access and modification times of a file +// +//export libfuse_utimens +func libfuse_utimens(path *C.char, tv *C.timespec_t, fi *C.fuse_file_info_t) C.int { + name := trimFusePath(path) + name = common.NormalizeObjectName(name) + log.Trace("Libfuse::libfuse_utimens : %s", name) + // TODO: is the conversion from [2]timespec to *timespec ok? + // TODO: Implement + // For now this returns 0 to allow touch to work correctly + return 0 +} + +// cloudfuse_cache_update refresh the file-cache policy for this file +// +//export cloudfuse_cache_update +func cloudfuse_cache_update(path *C.char) C.int { + name := trimFusePath(path) + name = common.NormalizeObjectName(name) + go fuseFS.NextComponent().FileUsed(name) //nolint + return 0 +} diff --git a/component/libfuse/libfuse_handler_test_wrapper.go b/component/libfuse/libfuse_handler_test_wrapper.go new file mode 100644 index 000000000..9c049e097 --- /dev/null +++ b/component/libfuse/libfuse_handler_test_wrapper.go @@ -0,0 +1,703 @@ +//go:build linux && !fuse2 + +/* + Licensed under the MIT License . + + Copyright © 2023-2024 Seagate Technology LLC and/or its Affiliates + Copyright © 2020-2024 Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE +*/ + +package libfuse + +// #cgo CFLAGS: -DFUSE_USE_VERSION=39 -D_FILE_OFFSET_BITS=64 +// #cgo LDFLAGS: -lfuse3 -ldl +// #include "libfuse_wrapper.h" +import "C" +import ( + "errors" + "fmt" + "io/fs" + "strings" + "syscall" + "unsafe" + + "github.com/Seagate/cloudfuse/common" + "github.com/Seagate/cloudfuse/common/config" + "github.com/Seagate/cloudfuse/common/log" + "github.com/Seagate/cloudfuse/internal" + "github.com/Seagate/cloudfuse/internal/handlemap" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type libfuseTestSuite struct { + suite.Suite + assert *assert.Assertions + libfuse *Libfuse + mockCtrl *gomock.Controller + mock *internal.MockComponent +} + +type fileHandle struct { + fd uint64 + obj uint64 +} + +var emptyConfig = "" +var defaultSize = int64(0) +var defaultMode = 0777 + +func newTestLibfuse(next internal.Component, configuration string) *Libfuse { + err := config.ReadConfigFromReader(strings.NewReader(configuration)) + if err != nil { + panic(fmt.Sprintf("Unable to read config from reader: %v", err)) + } + libfuse := NewLibfuseComponent() + libfuse.SetNextComponent(next) + err = libfuse.Configure(true) + if err != nil { + panic(fmt.Sprintf("Unable to configure for testing: %v", err)) + } + + return libfuse.(*Libfuse) +} + +func (suite *libfuseTestSuite) SetupTest() { + err := log.SetDefaultLogger("silent", common.LogConfig{}) + if err != nil { + panic("Unable to set silent logger as default.") + } + suite.setupTestHelper(emptyConfig) +} + +func (suite *libfuseTestSuite) setupTestHelper(config string) { + suite.assert = assert.New(suite.T()) + + suite.mockCtrl = gomock.NewController(suite.T()) + suite.mock = internal.NewMockComponent(suite.mockCtrl) + suite.libfuse = newTestLibfuse(suite.mock, config) + fuseFS = suite.libfuse + // suite.libfuse.Start(context.Background()) +} + +func (suite *libfuseTestSuite) cleanupTest() { + // suite.libfuse.Stop() + suite.mockCtrl.Finish() +} + +func testMkDir(suite *libfuseTestSuite) { + defer suite.cleanupTest() + name := "path" + path := C.CString("/" + name) + defer C.free(unsafe.Pointer(path)) + mode := fs.FileMode(0775) + options := internal.CreateDirOptions{Name: name, Mode: mode} + suite.mock.EXPECT().CreateDir(options).Return(nil) + + err := libfuse_mkdir(path, 0775) + suite.assert.Equal(C.int(0), err) +} + +func testMkDirError(suite *libfuseTestSuite) { + defer suite.cleanupTest() + name := "path" + path := C.CString("/" + name) + defer C.free(unsafe.Pointer(path)) + mode := fs.FileMode(0775) + options := internal.CreateDirOptions{Name: name, Mode: mode} + suite.mock.EXPECT().CreateDir(options).Return(errors.New("failed to create directory")) + + err := libfuse_mkdir(path, 0775) + suite.assert.Equal(C.int(-C.EIO), err) +} + +// testMkDirErrorAttrExist only runs on Windows to test the case that the directory already exists +// Sine fuse3 doesn't work on windows, do nothing +func testMkDirErrorAttrExist(suite *libfuseTestSuite) { +} + +// TODO: ReadDir test + +func testRmDir(suite *libfuseTestSuite) { + defer suite.cleanupTest() + name := "path" + path := C.CString("/" + name) + defer C.free(unsafe.Pointer(path)) + isDirEmptyOptions := internal.IsDirEmptyOptions{Name: name} + suite.mock.EXPECT().IsDirEmpty(isDirEmptyOptions).Return(true) + deleteDirOptions := internal.DeleteDirOptions{Name: name} + suite.mock.EXPECT().DeleteDir(deleteDirOptions).Return(nil) + + err := libfuse_rmdir(path) + suite.assert.Equal(C.int(0), err) +} + +func testRmDirNotEmpty(suite *libfuseTestSuite) { + defer suite.cleanupTest() + name := "path" + path := C.CString("/" + name) + defer C.free(unsafe.Pointer(path)) + isDirEmptyOptions := internal.IsDirEmptyOptions{Name: name} + suite.mock.EXPECT().IsDirEmpty(isDirEmptyOptions).Return(false) + + err := libfuse_rmdir(path) + suite.assert.Equal(C.int(-C.ENOTEMPTY), err) +} + +func testRmDirError(suite *libfuseTestSuite) { + defer suite.cleanupTest() + name := "path" + path := C.CString("/" + name) + defer C.free(unsafe.Pointer(path)) + isDirEmptyOptions := internal.IsDirEmptyOptions{Name: name} + suite.mock.EXPECT().IsDirEmpty(isDirEmptyOptions).Return(true) + deleteDirOptions := internal.DeleteDirOptions{Name: name} + suite.mock.EXPECT().DeleteDir(deleteDirOptions).Return(errors.New("failed to delete directory")) + + err := libfuse_rmdir(path) + suite.assert.Equal(C.int(-C.EIO), err) +} + +func testCreate(suite *libfuseTestSuite) { + defer suite.cleanupTest() + name := "path" + path := C.CString("/" + name) + defer C.free(unsafe.Pointer(path)) + mode := fs.FileMode(0775) + info := &C.fuse_file_info_t{} + options := internal.CreateFileOptions{Name: name, Mode: mode} + suite.mock.EXPECT().CreateFile(options).Return(&handlemap.Handle{}, nil) + + err := libfuse_create(path, 0775, info) + suite.assert.Equal(C.int(0), err) + option := internal.GetAttrOptions{Name: name} + suite.mock.EXPECT().GetAttr(option).Return(&internal.ObjAttr{}, nil) + stbuf := &C.stat_t{} + err = libfuse_getattr(path, stbuf, &C.fuse_file_info_t{}) + suite.assert.Equal(C.int(0), err) + suite.assert.Equal(stbuf.st_mtim.tv_nsec, C.long(0)) + suite.assert.NotEqual(stbuf.st_mtim.tv_sec, C.long(0)) +} + +func testCreateError(suite *libfuseTestSuite) { + defer suite.cleanupTest() + name := "path" + path := C.CString("/" + name) + defer C.free(unsafe.Pointer(path)) + mode := fs.FileMode(0775) + info := &C.fuse_file_info_t{} + options := internal.CreateFileOptions{Name: name, Mode: mode} + suite.mock.EXPECT().CreateFile(options).Return(&handlemap.Handle{}, errors.New("failed to create file")) + + err := libfuse_create(path, 0775, info) + suite.assert.Equal(C.int(-C.EIO), err) +} + +func testOpen(suite *libfuseTestSuite) { + defer suite.cleanupTest() + name := "path" + path := C.CString("/" + name) + defer C.free(unsafe.Pointer(path)) + mode := fs.FileMode(fuseFS.filePermission) + flags := C.O_RDWR & 0xffffffff + info := &C.fuse_file_info_t{} + info.flags = C.O_RDWR + options := internal.OpenFileOptions{Name: name, Flags: flags, Mode: mode} + suite.mock.EXPECT().OpenFile(options).Return(&handlemap.Handle{}, nil) + + err := libfuse_open(path, info) + suite.assert.Equal(C.int(0), err) +} + +func testOpenSyncDirectFlag(suite *libfuseTestSuite) { + defer suite.cleanupTest() + name := "path" + path := C.CString("/" + name) + defer C.free(unsafe.Pointer(path)) + mode := fs.FileMode(fuseFS.filePermission) + flags := C.O_RDWR & 0xffffffff + info := &C.fuse_file_info_t{} + info.flags = C.O_RDWR | C.O_SYNC | C.__O_DIRECT + options := internal.OpenFileOptions{Name: name, Flags: flags, Mode: mode} + suite.mock.EXPECT().OpenFile(options).Return(&handlemap.Handle{}, nil) + + err := libfuse_open(path, info) + suite.assert.Equal(C.int(0), err) + suite.assert.Equal(C.int(0), info.flags&C.O_SYNC) + suite.assert.Equal(C.int(0), info.flags&C.__O_DIRECT) +} + +// WriteBack caching and ignore-open-flags enabled by default +func testOpenAppendFlagDefault(suite *libfuseTestSuite) { + defer suite.cleanupTest() + suite.libfuse.ignoreOpenFlags = false + + name := "path" + path := C.CString("/" + name) + defer C.free(unsafe.Pointer(path)) + info := &C.fuse_file_info_t{} + info.flags = C.O_RDWR | C.O_APPEND + + err := libfuse_open(path, info) + suite.assert.Equal(C.int(-C.EINVAL), err) + + info.flags = C.O_WRONLY | C.O_APPEND + + err = libfuse_open(path, info) + suite.assert.Equal(C.int(-C.EINVAL), err) +} + +func testOpenAppendFlagDisableWritebackCache(suite *libfuseTestSuite) { + defer suite.cleanupTest() + suite.cleanupTest() // clean up the default libfuse generated + config := "libfuse:\n disable-writeback-cache: true\n" + suite.setupTestHelper(config) // setup a new libfuse with a custom config (clean up will occur after the test as usual) + suite.assert.True(suite.libfuse.disableWritebackCache) + + name := "path" + path := C.CString("/" + name) + defer C.free(unsafe.Pointer(path)) + mode := fs.FileMode(fuseFS.filePermission) + flags := C.O_RDWR | C.O_APPEND&0xffffffff + info := &C.fuse_file_info_t{} + info.flags = C.O_RDWR | C.O_APPEND + options := internal.OpenFileOptions{Name: name, Flags: flags, Mode: mode} + suite.mock.EXPECT().OpenFile(options).Return(&handlemap.Handle{}, nil) + + err := libfuse_open(path, info) + suite.assert.Equal(C.int(0), err) + + flags = C.O_WRONLY | C.O_APPEND&0xffffffff + info = &C.fuse_file_info_t{} + info.flags = C.O_WRONLY | C.O_APPEND + options = internal.OpenFileOptions{Name: name, Flags: flags, Mode: mode} + suite.mock.EXPECT().OpenFile(options).Return(&handlemap.Handle{}, nil) + + err = libfuse_open(path, info) + suite.assert.Equal(C.int(0), err) +} + +func testOpenAppendFlagIgnoreAppendFlag(suite *libfuseTestSuite) { + defer suite.cleanupTest() + suite.cleanupTest() // clean up the default libfuse generated + config := "libfuse:\n ignore-open-flags: true\n" + suite.setupTestHelper(config) // setup a new libfuse with a custom config (clean up will occur after the test as usual) + suite.assert.True(suite.libfuse.ignoreOpenFlags) + + name := "path" + path := C.CString("/" + name) + defer C.free(unsafe.Pointer(path)) + mode := fs.FileMode(fuseFS.filePermission) + flags := C.O_RDWR & 0xffffffff + info := &C.fuse_file_info_t{} + info.flags = C.O_RDWR | C.O_APPEND + options := internal.OpenFileOptions{Name: name, Flags: flags, Mode: mode} + suite.mock.EXPECT().OpenFile(options).Return(&handlemap.Handle{}, nil) + + err := libfuse_open(path, info) + suite.assert.Equal(C.int(0), err) + suite.assert.Equal(C.int(0), info.flags&C.O_APPEND) + + flags = C.O_RDWR & 0xffffffff + info = &C.fuse_file_info_t{} + info.flags = C.O_WRONLY | C.O_APPEND + options = internal.OpenFileOptions{Name: name, Flags: flags, Mode: mode} + suite.mock.EXPECT().OpenFile(options).Return(&handlemap.Handle{}, nil) + + err = libfuse_open(path, info) + suite.assert.Equal(C.int(0), err) + suite.assert.Equal(C.int(0), info.flags&C.O_APPEND) + + flags = C.O_RDWR & 0xffffffff + info = &C.fuse_file_info_t{} + info.flags = C.O_WRONLY + options = internal.OpenFileOptions{Name: name, Flags: flags, Mode: mode} + suite.mock.EXPECT().OpenFile(options).Return(&handlemap.Handle{}, nil) + + err = libfuse_open(path, info) + suite.assert.Equal(C.int(0), err) +} + +func testOpenNotExists(suite *libfuseTestSuite) { + defer suite.cleanupTest() + name := "path" + path := C.CString("/" + name) + defer C.free(unsafe.Pointer(path)) + mode := fs.FileMode(fuseFS.filePermission) + flags := C.O_RDWR & 0xffffffff + info := &C.fuse_file_info_t{} + info.flags = C.O_RDWR + options := internal.OpenFileOptions{Name: name, Flags: flags, Mode: mode} + suite.mock.EXPECT().OpenFile(options).Return(&handlemap.Handle{}, syscall.ENOENT) + + err := libfuse_open(path, info) + suite.assert.Equal(C.int(-C.ENOENT), err) +} + +func testOpenError(suite *libfuseTestSuite) { + defer suite.cleanupTest() + name := "path" + path := C.CString("/" + name) + defer C.free(unsafe.Pointer(path)) + mode := fs.FileMode(fuseFS.filePermission) + flags := C.O_RDWR & 0xffffffff + info := &C.fuse_file_info_t{} + info.flags = C.O_RDWR + options := internal.OpenFileOptions{Name: name, Flags: flags, Mode: mode} + suite.mock.EXPECT().OpenFile(options).Return(&handlemap.Handle{}, errors.New("failed to open a file")) + + err := libfuse_open(path, info) + suite.assert.Equal(C.int(-C.EIO), err) +} + +func testTruncate(suite *libfuseTestSuite) { + defer suite.cleanupTest() + name := "path" + path := C.CString("/" + name) + defer C.free(unsafe.Pointer(path)) + size := int64(1024) + options := internal.TruncateFileOptions{Name: name, Size: size} + suite.mock.EXPECT().TruncateFile(options).Return(nil) + + err := libfuse_truncate(path, C.long(size), nil) + suite.assert.Equal(C.int(0), err) +} + +func testTruncateError(suite *libfuseTestSuite) { + defer suite.cleanupTest() + name := "path" + path := C.CString("/" + name) + defer C.free(unsafe.Pointer(path)) + size := int64(1024) + options := internal.TruncateFileOptions{Name: name, Size: size} + suite.mock.EXPECT().TruncateFile(options).Return(errors.New("failed to truncate file")) + + err := libfuse_truncate(path, C.long(size), nil) + suite.assert.Equal(C.int(-C.EIO), err) +} + +func testUnlink(suite *libfuseTestSuite) { + defer suite.cleanupTest() + name := "path" + path := C.CString("/" + name) + defer C.free(unsafe.Pointer(path)) + options := internal.DeleteFileOptions{Name: name} + suite.mock.EXPECT().DeleteFile(options).Return(nil) + + err := libfuse_unlink(path) + suite.assert.Equal(C.int(0), err) +} + +func testUnlinkNotExists(suite *libfuseTestSuite) { + defer suite.cleanupTest() + name := "path" + path := C.CString("/" + name) + defer C.free(unsafe.Pointer(path)) + options := internal.DeleteFileOptions{Name: name} + suite.mock.EXPECT().DeleteFile(options).Return(syscall.ENOENT) + + err := libfuse_unlink(path) + suite.assert.Equal(C.int(-C.ENOENT), err) +} + +func testUnlinkError(suite *libfuseTestSuite) { + defer suite.cleanupTest() + name := "path" + path := C.CString("/" + name) + defer C.free(unsafe.Pointer(path)) + options := internal.DeleteFileOptions{Name: name} + suite.mock.EXPECT().DeleteFile(options).Return(errors.New("failed to delete file")) + + err := libfuse_unlink(path) + suite.assert.Equal(C.int(-C.EIO), err) +} + +// Rename + +func testSymlink(suite *libfuseTestSuite) { + defer suite.cleanupTest() + name := "path" + target := "target" + path := C.CString("/" + name) + defer C.free(unsafe.Pointer(path)) + t := C.CString(target) + defer C.free(unsafe.Pointer(t)) + options := internal.CreateLinkOptions{Name: name, Target: target} + suite.mock.EXPECT().CreateLink(options).Return(nil) + + err := libfuse_symlink(t, path) + suite.assert.Equal(C.int(0), err) +} + +func testSymlinkError(suite *libfuseTestSuite) { + defer suite.cleanupTest() + name := "path" + target := "target" + path := C.CString("/" + name) + defer C.free(unsafe.Pointer(path)) + t := C.CString(target) + defer C.free(unsafe.Pointer(t)) + options := internal.CreateLinkOptions{Name: name, Target: target} + suite.mock.EXPECT().CreateLink(options).Return(errors.New("failed to create link")) + + err := libfuse_symlink(t, path) + suite.assert.Equal(C.int(-C.EIO), err) +} + +func testReadLink(suite *libfuseTestSuite) { + defer suite.cleanupTest() + name := "path" + path := C.CString("/" + name) + defer C.free(unsafe.Pointer(path)) + options := internal.ReadLinkOptions{Name: name} + suite.mock.EXPECT().ReadLink(options).Return("target", nil) + + // https://stackoverflow.com/questions/41953619/how-to-initialise-empty-c-cstring-in-cgo + buf := C.CString("") + err := libfuse_readlink(path, buf, 7) + suite.assert.Equal(C.int(0), err) + suite.assert.Equal("target", C.GoString(buf)) +} + +func testReadLinkNotExists(suite *libfuseTestSuite) { + defer suite.cleanupTest() + name := "path" + path := C.CString("/" + name) + defer C.free(unsafe.Pointer(path)) + options := internal.ReadLinkOptions{Name: name} + suite.mock.EXPECT().ReadLink(options).Return("", syscall.ENOENT) + + buf := C.CString("") + err := libfuse_readlink(path, buf, 7) + suite.assert.Equal(C.int(-C.ENOENT), err) + suite.assert.NotEqual("target", C.GoString(buf)) +} + +func testReadLinkError(suite *libfuseTestSuite) { + defer suite.cleanupTest() + name := "path" + path := C.CString("/" + name) + defer C.free(unsafe.Pointer(path)) + options := internal.ReadLinkOptions{Name: name} + suite.mock.EXPECT().ReadLink(options).Return("", errors.New("failed to read link")) + + buf := C.CString("") + err := libfuse_readlink(path, buf, 7) + suite.assert.Equal(C.int(-C.EIO), err) + suite.assert.NotEqual("target", C.GoString(buf)) +} + +func testFsync(suite *libfuseTestSuite) { + defer suite.cleanupTest() + name := "path" + path := C.CString("/" + name) + defer C.free(unsafe.Pointer(path)) + mode := fs.FileMode(fuseFS.filePermission) + flags := C.O_RDWR & 0xffffffff + info := &C.fuse_file_info_t{} + info.flags = C.O_RDWR + handle := &handlemap.Handle{} + openOptions := internal.OpenFileOptions{Name: name, Flags: flags, Mode: mode} + suite.mock.EXPECT().OpenFile(openOptions).Return(handle, nil) + libfuse_open(path, info) + suite.assert.NotEqual(C.ulong(0), info.fh) + + // libfuse component will return back handle in form of an integer value + // that needs to be converted back to a pointer to a handle object + fobj := (*fileHandle)(unsafe.Pointer(uintptr(info.fh))) + handle = (*handlemap.Handle)(unsafe.Pointer(uintptr(fobj.obj))) + + options := internal.SyncFileOptions{Handle: handle} + suite.mock.EXPECT().SyncFile(options).Return(nil) + + err := libfuse_fsync(path, C.int(0), info) + suite.assert.Equal(C.int(0), err) +} + +func testFsyncHandleError(suite *libfuseTestSuite) { + defer suite.cleanupTest() + name := "path" + path := C.CString("/" + name) + defer C.free(unsafe.Pointer(path)) + info := &C.fuse_file_info_t{} + info.flags = C.O_RDWR + + err := libfuse_fsync(path, C.int(0), info) + suite.assert.Equal(C.int(-C.EIO), err) +} + +func testFsyncError(suite *libfuseTestSuite) { + defer suite.cleanupTest() + name := "path" + path := C.CString("/" + name) + defer C.free(unsafe.Pointer(path)) + mode := fs.FileMode(fuseFS.filePermission) + flags := C.O_RDWR & 0xffffffff + info := &C.fuse_file_info_t{} + info.flags = C.O_RDWR + handle := &handlemap.Handle{} + openOptions := internal.OpenFileOptions{Name: name, Flags: flags, Mode: mode} + suite.mock.EXPECT().OpenFile(openOptions).Return(handle, nil) + libfuse_open(path, info) + suite.assert.NotEqual(C.ulong(0), info.fh) + + // libfuse component will return back handle in form of an integer value + // that needs to be converted back to a pointer to a handle object + fobj := (*fileHandle)(unsafe.Pointer(uintptr(info.fh))) + handle = (*handlemap.Handle)(unsafe.Pointer(uintptr(fobj.obj))) + + options := internal.SyncFileOptions{Handle: handle} + suite.mock.EXPECT().SyncFile(options).Return(errors.New("failed to sync file")) + + err := libfuse_fsync(path, C.int(0), info) + suite.assert.Equal(C.int(-C.EIO), err) +} + +func testFsyncDir(suite *libfuseTestSuite) { + defer suite.cleanupTest() + name := "path" + path := C.CString("/" + name) + defer C.free(unsafe.Pointer(path)) + options := internal.SyncDirOptions{Name: name} + suite.mock.EXPECT().SyncDir(options).Return(nil) + + err := libfuse_fsyncdir(path, C.int(0), nil) + suite.assert.Equal(C.int(0), err) +} + +func testFsyncDirError(suite *libfuseTestSuite) { + defer suite.cleanupTest() + name := "path" + path := C.CString("/" + name) + defer C.free(unsafe.Pointer(path)) + options := internal.SyncDirOptions{Name: name} + suite.mock.EXPECT().SyncDir(options).Return(errors.New("failed to sync dir")) + + err := libfuse_fsyncdir(path, C.int(0), nil) + suite.assert.Equal(C.int(-C.EIO), err) +} + +func testChmod(suite *libfuseTestSuite) { + defer suite.cleanupTest() + name := "path" + path := C.CString("/" + name) + defer C.free(unsafe.Pointer(path)) + mode := fs.FileMode(0775) + options := internal.ChmodOptions{Name: name, Mode: mode} + suite.mock.EXPECT().Chmod(options).Return(nil) + + err := libfuse_chmod(path, 0775, nil) + suite.assert.Equal(C.int(0), err) +} + +func testChmodNotExists(suite *libfuseTestSuite) { + defer suite.cleanupTest() + name := "path" + path := C.CString("/" + name) + defer C.free(unsafe.Pointer(path)) + mode := fs.FileMode(0775) + options := internal.ChmodOptions{Name: name, Mode: mode} + suite.mock.EXPECT().Chmod(options).Return(syscall.ENOENT) + + err := libfuse_chmod(path, 0775, nil) + suite.assert.Equal(C.int(-C.ENOENT), err) +} + +func testStatFs(suite *libfuseTestSuite) { + defer suite.cleanupTest() + path := C.CString("/") + defer C.free(unsafe.Pointer(path)) + suite.mock.EXPECT().StatFs().Return(&common.Statfs_t{Frsize: 1, + Blocks: 2, Bavail: 3, Bfree: 4}, true, nil) + buf := &C.statvfs_t{} + libfuse_statfs(path, buf) + + suite.assert.Equal(1, int(buf.f_frsize)) + suite.assert.Equal(2, int(buf.f_blocks)) + suite.assert.Equal(3, int(buf.f_bavail)) + suite.assert.Equal(4, int(buf.f_bfree)) +} + +func testStatFsNotPopulated(suite *libfuseTestSuite) { + defer suite.cleanupTest() + path := C.CString("/") + defer C.free(unsafe.Pointer(path)) + suite.mock.EXPECT().StatFs().Return(nil, false, nil) + buf := &C.statvfs_t{} + libfuse_statfs(path, buf) + + // By default these are all 0, so they should be populated by the system + // and thus each larger than 0 + suite.assert.Positive(buf.f_frsize) + suite.assert.Positive(buf.f_blocks) + suite.assert.Positive(buf.f_bavail) + suite.assert.Positive(buf.f_bfree) + suite.assert.Positive(buf.f_bsize) + suite.assert.Positive(buf.f_files) + suite.assert.Positive(buf.f_ffree) + suite.assert.Positive(buf.f_namemax) +} + +func testStatFsError(suite *libfuseTestSuite) { + defer suite.cleanupTest() + path := C.CString("/") + suite.mock.EXPECT().StatFs().Return(nil, false, errors.New("Error")) + buf := &C.statvfs_t{} + err := libfuse_statfs(path, buf) + suite.assert.Equal(C.int(-C.EIO), err) +} + +func testChmodError(suite *libfuseTestSuite) { + defer suite.cleanupTest() + name := "path" + path := C.CString("/" + name) + defer C.free(unsafe.Pointer(path)) + mode := fs.FileMode(0775) + options := internal.ChmodOptions{Name: name, Mode: mode} + suite.mock.EXPECT().Chmod(options).Return(errors.New("failed to chmod")) + + err := libfuse_chmod(path, 0775, nil) + suite.assert.Equal(C.int(-C.EIO), err) +} + +func testChown(suite *libfuseTestSuite) { + defer suite.cleanupTest() + name := "path" + path := C.CString("/" + name) + defer C.free(unsafe.Pointer(path)) + group := C.uint(5) + owner := C.uint(4) + + err := libfuse_chown(path, owner, group, nil) + suite.assert.Equal(C.int(0), err) +} + +func testUtimens(suite *libfuseTestSuite) { + defer suite.cleanupTest() + name := "path" + path := C.CString("/" + name) + defer C.free(unsafe.Pointer(path)) + + err := libfuse_utimens(path, nil, nil) + suite.assert.Equal(C.int(0), err) +} diff --git a/component/libfuse/libfuse_wrapper.h b/component/libfuse/libfuse_wrapper.h new file mode 100644 index 000000000..3f1013fde --- /dev/null +++ b/component/libfuse/libfuse_wrapper.h @@ -0,0 +1,165 @@ +/* + Licensed under the MIT License . + + Copyright © 2023-2024 Seagate Technology LLC and/or its Affiliates + Copyright © 2020-2024 Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE +*/ + +#ifndef __LIBFUSE_H__ +#define __LIBFUSE_H__ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "libfuse_defs.h" +#include "native_file_io.h" + +// Method to populate the fuse structure with our callback methods +static int populate_callbacks(fuse_operations_t *opt) +{ + opt->destroy = (void (*)(void *))libfuse_destroy; + + opt->statfs = (int (*)(const char *path, statvfs_t *stbuf))libfuse_statfs; + + opt->mkdir = (int (*)(const char *path, mode_t mode))libfuse_mkdir; + opt->rmdir = (int (*)(const char *path))libfuse_rmdir; + + opt->opendir = (int (*)(const char *path, fuse_file_info_t *fi))libfuse_opendir; + opt->releasedir = (int (*)(const char *path, fuse_file_info_t *fi))libfuse_releasedir; + + opt->create = (int (*)(const char *path, mode_t mode, fuse_file_info_t *fi))libfuse_create; + opt->open = (int (*)(const char *path, fuse_file_info_t *fi))libfuse_open; + +// These are methods declared in C to do read/write operation directly on file for better performance +#if 1 + opt->read = (int (*)(const char *path, char *buf, size_t, off_t, fuse_file_info_t *))native_read_file; + opt->write = (int (*)(const char *path, const char *buf, size_t, off_t, fuse_file_info_t *))native_write_file; + opt->flush = (int (*)(const char *path, fuse_file_info_t *fi))native_flush_file; +#else + opt->read = (int (*)(const char *path, char *buf, size_t, off_t, fuse_file_info_t *))libfuse_read; + opt->write = (int (*)(const char *path, const char *buf, size_t, off_t, fuse_file_info_t *))libfuse_write; + opt->flush = (int (*)(const char *path, fuse_file_info_t *fi))libfuse_flush; +#endif + + opt->release = (int (*)(const char *path, fuse_file_info_t *fi))libfuse_release; + + opt->unlink = (int (*)(const char *path))libfuse_unlink; + + opt->symlink = (int (*)(const char *from, const char *to))libfuse_symlink; + opt->readlink = (int (*)(const char *path, char *buf, size_t size))libfuse_readlink; + + opt->fsync = (int (*)(const char *path, int, fuse_file_info_t *fi))libfuse_fsync; + opt->fsyncdir = (int (*)(const char *path, int, fuse_file_info_t *))libfuse_fsyncdir; + +#ifdef __FUSE2__ + opt->init = (void *(*)(fuse_conn_info_t *))libfuse2_init; + opt->getattr = (int (*)(const char *, stat_t *))libfuse2_getattr; + opt->readdir = (int (*)(const char *path, void *buf, fuse_fill_dir_t filler, off_t, fuse_file_info_t *))libfuse2_readdir; + opt->truncate = (int (*)(const char *path, off_t off))libfuse2_truncate; + opt->rename = (int (*)(const char *src, const char *dst))libfuse2_rename; + opt->chmod = (int (*)(const char *path, mode_t mode))libfuse2_chmod; + opt->chown = (int (*)(const char *path, uid_t uid, gid_t gid))libfuse2_chown; + opt->utimens = (int (*)(const char *path, const timespec_t tv[2]))libfuse2_utimens; +#else + opt->init = (void *(*)(fuse_conn_info_t *, fuse_config_t *))libfuse_init; + opt->getattr = (int (*)(const char *, stat_t *, fuse_file_info_t *))libfuse_getattr; + opt->readdir = (int (*)(const char *path, void *buf, fuse_fill_dir_t filler, off_t, fuse_file_info_t *, + fuse_readdir_flags_t))libfuse_readdir; + opt->truncate = (int (*)(const char *path, off_t off, fuse_file_info_t *fi))libfuse_truncate; + opt->rename = (int (*)(const char *src, const char *dst, unsigned int flags))libfuse_rename; + opt->chmod = (int (*)(const char *path, mode_t mode, fuse_file_info_t *fi))libfuse_chmod; + opt->chown = (int (*)(const char *path, uid_t uid, gid_t gid, fuse_file_info_t *fi))libfuse_chown; + opt->utimens = (int (*)(const char *path, const timespec_t tv[2], fuse_file_info_t *fi))libfuse_utimens; +#endif + + return 0; +} + +static fuse_options_t fuse_opts; +static bool context_populated = false; + +// Main method to start fuse loop which will fork and send us callbacks +static int start_fuse(fuse_args_t *args, fuse_operations_t *opt) +{ + return fuse_main(args->argc, args->argv, opt, NULL); +} + +// This method is not declared in Go because we are just doing "/" statfs as dummy operation +static int populate_statfs(const char *path, struct statvfs *stbuf) +{ + // return tmp path stats + errno = 0; + int res = statvfs("/", stbuf); + if (res == -1) + return -errno; + + return 0; +} + +// Get uid and gid from fuse context +static void populate_uid_gid() +{ + if (!context_populated) + { + fuse_opts.uid = fuse_get_context()->uid; + fuse_opts.gid = fuse_get_context()->gid; + context_populated = true; + } +} + +// Properties for root (/) are static so just hardcoding them here +static int get_root_properties(stat_t *stbuf) +{ + populate_uid_gid(); + + stbuf->st_mode = S_IFDIR | 0777; + stbuf->st_uid = fuse_opts.uid; + stbuf->st_gid = fuse_opts.gid; + stbuf->st_nlink = 2; + stbuf->st_size = 4096; + stbuf->st_mtime = time(NULL); + stbuf->st_atime = stbuf->st_mtime; + stbuf->st_ctime = stbuf->st_mtime; + return 0; +} + +static int fill_dir_entry(fuse_fill_dir_t filler, void *buf, char *name, stat_t *stbuf, off_t off) +{ + return filler(buf, name, stbuf, off +#ifndef __FUSE2__ + , + (fuse_fill_dir_flags_t)fill_dir_plus +#endif + ); +} + +#endif //__LIBFUSE_H__ \ No newline at end of file diff --git a/component/libfuse/native_file_io.h b/component/libfuse/native_file_io.h new file mode 100644 index 000000000..6bce14313 --- /dev/null +++ b/component/libfuse/native_file_io.h @@ -0,0 +1,235 @@ +/* + Licensed under the MIT License . + + Copyright © 2023-2024 Seagate Technology LLC and/or its Affiliates + Copyright © 2020-2024 Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE +*/ + +#ifndef __NATIVE_FILE_IO_H__ +#define __NATIVE_FILE_IO_H__ + +// Every read-write operation is counted and after N operations send a call up to update cache policy +#define CACHE_UPDATE_COUNTER 100 + +// Structure that describes file-handle object returned back to libfuse +typedef struct +{ + uint64_t fd; // Unix FD for this file + uint64_t obj; // Handlemap.Handle object representing this handle + uint16_t cnt; // Number of read-write operations done on this handle + uint8_t dirty; // A write operation was performed on this handle +} file_handle_t; + +// allocate_native_file_object : Allocate a native C-struct to hold handle map object and unix FD +static file_handle_t *allocate_native_file_object(uint64_t fd, uint64_t obj, uint64_t file_size) +{ + // Called on open / create calls from libfuse component + file_handle_t *fobj = (file_handle_t *)malloc(sizeof(file_handle_t)); + if (fobj) + { + memset(fobj, 0, sizeof(file_handle_t)); + fobj->fd = fd; + fobj->obj = obj; + } + + return fobj; +} + +// release_native_file_object : Release the native C-struct for handle +static void release_native_file_object(fuse_file_info_t *fi) +{ + // Called on close operation from libfuse component + file_handle_t *handle_obj = (file_handle_t *)fi->fh; + if (handle_obj) + { + free(handle_obj); + } +} + +// native_pread : Do pread on file directly without involving any Go code +static int native_pread(char *path, char *buf, size_t size, off_t offset, file_handle_t *handle_obj) +{ + errno = 0; + int res = pread(handle_obj->fd, buf, size, offset); + if (res == -1) + res = -errno; + +#if 0 + handle_obj->cnt++; + if (!(handle_obj->cnt % CACHE_UPDATE_COUNTER)) { + // Time to send a call up to update the cache + cloudfuse_cache_update(path); + handle_obj->cnt = 0; + } +#endif + + return res; +} + +// native_pwrite : Do pwrite on file directly without involving any Go code +static int native_pwrite(char *path, char *buf, size_t size, off_t offset, file_handle_t *handle_obj) +{ + errno = 0; + int res = pwrite(handle_obj->fd, buf, size, offset); + if (res == -1) + res = -errno; + + // Increment the operation counter and mark a write was done on this handle + handle_obj->dirty = 1; + handle_obj->cnt++; + if (!(handle_obj->cnt % CACHE_UPDATE_COUNTER)) + { + // Time to send a call up to update the cache + cloudfuse_cache_update(path); + handle_obj->cnt = 0; + } + + return res; +} + +// native_read_file : Read callback to decide whether to natively read or punt call to Go code +static int native_read_file(char *path, char *buf, size_t size, off_t offset, fuse_file_info_t *fi) +{ + file_handle_t *handle_obj = (file_handle_t *)fi->fh; +#if 0 + return libfuse_read(path, buf, size, offset, fi); +#endif + + if (handle_obj->fd == 0) + { + return libfuse_read(path, buf, size, offset, fi); + } + + return native_pread(path, buf, size, offset, handle_obj); +} + +// native_write_file : Write callback to decide whether to natively write or punt call to Go code +static int native_write_file(char *path, char *buf, size_t size, off_t offset, fuse_file_info_t *fi) +{ + file_handle_t *handle_obj = (file_handle_t *)fi->fh; +#if 0 + return libfuse_write(path, buf, size, offset, fi); +#endif + + if (handle_obj->fd == 0) + { + return libfuse_write(path, buf, size, offset, fi); + } + + return native_pwrite(path, buf, size, offset, handle_obj); +} + +// native_flush_file : Flush the file natively and call flush up in the pipeline to upload this file +static int native_flush_file(char *path, fuse_file_info_t *fi) +{ + file_handle_t *handle_obj = (file_handle_t *)fi->fh; + int ret = libfuse_flush(path, fi); + if (ret == 0) + { + // As file is flushed and uploaded, reset the dirty bit here + handle_obj->dirty = 0; + } + + return ret; +} + +#ifdef ENABLE_READ_AHEAD +// read_ahead_handler : Method to serve read call from read-ahead buffer if possible +static int read_ahead_handler(char *path, char *buf, size_t size, off_t offset, file_handle_t *handle_obj) +{ + int new_read = 0; + + /* Random read determination logic : + handle_obj->random_reads : is counter used for this + - For every sequential read decrement this counter by 1 + - For every new read from physical file (random read or buffer refresh) increment the counter by 2 + - At any point if the counter value is > 5 then caller will disable read-ahead on this handle + + : If file is being read sequentially then counter will be negative and a buffer refresh will not skew the counter much + : If file is read sequentially and later application moves to random read, at some point we will disable read-ahead logic + : If file is read randomly then counter will be positive and we will disable read-ahead after 2-3 reads + : If file is read randomly first and then sequentially then we assume it will be random read and disable the read-ahead + */ + + if ((handle_obj->buff_start == 0 && handle_obj->buff_end == 0) || + offset < handle_obj->buff_start || + offset >= handle_obj->buff_end) + { + // Either this is first read call or read is outside the current buffer boundary + // So we need to read a fresh buffer from physical file + new_read = 1; + handle_obj->random_reads += 2; + } + else + { + handle_obj->random_reads--; + } + + if (new_read) + { + // We need to refresh the data from file + int read = native_pread(path, handle_obj->buff, RA_BLOCK_SIZE, offset, handle_obj); + FILE *fp = fopen("cloudfuse_nat.log", "a"); + if (fp) + { + fprintf(fp, "File %s, Offset %ld, size %ld, new read %d\n", + path, offset, size, read); + fclose(fp); + } + + if (read <= 0) + { + // Error or EOF reached to just return 0 now + return read; + } + + handle_obj->buff_start = offset; + handle_obj->buff_end = offset + read; + } + + // Buffer is populated so calculate how much to copy from here now. + int start = offset - handle_obj->buff_start; + int left = (handle_obj->buff_end - offset); + int copy = (size > left) ? left : size; + + FILE *fp = fopen("cloudfuse_nat.log", "a"); + if (fp) + { + fprintf(fp, "File %s, Offset %ld, size %ld, buff start %ld, buff end %ld, start %d, left %d, copy %d\n", + path, offset, size, handle_obj->buff_start, handle_obj->buff_end, start, left, copy); + fclose(fp); + } + + memcpy(buf, (handle_obj->buff + start), copy); + + if (copy < size) + { + // Less then request data was copied so read from next offset again + // We need to handle this here because if we return less then size fuse is not asking from + // correct offset in next read, it just goes to offset + size only. + copy += read_ahead_handler(path, (buf + copy), (size - copy), (offset + copy), handle_obj); + } + + return copy; +} +#endif + +#endif //__NATIVE_FILE_IO_H__ \ No newline at end of file