├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── build.yml │ └── test.yml ├── .gitignore ├── .gitmodules ├── .vscode ├── launch.json └── settings.json ├── Contributors.asciidoc ├── License.txt ├── README.md ├── art ├── hubfs-glow.png ├── hubfs.afdesign ├── hubfs.png ├── wixbanner.afdesign ├── wixbanner.bmp ├── wixdialog-Beta.bmp ├── wixdialog-Gold.bmp └── wixdialog.afdesign ├── build ├── DigiCert High Assurance EV Root CA.crt ├── Makefile ├── gobuild.mac ├── hubfs.wxs ├── make └── make.cmd ├── doc ├── cap1.gif └── mapnet.png └── src ├── _tools ├── fetch.go ├── pmdump.go ├── ptfs.go └── unionfs.go ├── fs ├── hubfs │ ├── hubfs.go │ ├── hubfs_test.go │ ├── overlay.go │ └── shardfs.go ├── memfs │ └── memfs.go ├── nullfs │ └── nullfs.go ├── overlayfs │ └── overlayfs.go ├── port │ ├── port_darwin.go │ ├── port_linux.go │ ├── port_unix.go │ └── port_windows.go ├── ptfs │ └── ptfs.go └── unionfs │ ├── filemap.go │ ├── pathkey.go │ ├── pathkey_test.go │ ├── pathmap.go │ ├── pathmap_test.go │ ├── testfs_test.go │ ├── unionfs.go │ └── unionfs_test.go ├── git ├── git.go └── git_test.go ├── go.mod ├── go.sum ├── httputil └── httputil.go ├── main.go ├── prov ├── cache.go ├── client.go ├── emptyrepo.go ├── filter.go ├── filter_test.go ├── git.go ├── git_test.go ├── github.go ├── github_test.go ├── gitlab.go ├── package_test.go └── provider.go ├── pvt ├── pvt_ent.go └── util ├── event.go ├── event_test.go └── optlist.go /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | (Enter your PR description here.) 2 | 3 | ---- 4 | 5 | Before submitting this PR please review this checklist. Ideally all checkmarks should be checked upon submitting. (Use an x inside square brackets like so: [x]) 6 | 7 | - [ ] **Contributing**: You MUST read and be willing to accept the [CONTRIBUTOR AGREEMENT](https://github.com/winfsp/hubfs/blob/master/Contributors.asciidoc). The agreement gives joint copyright interests in your contributions to you and the original HUBFS author. If you have already accepted the [CONTRIBUTOR AGREEMENT](https://github.com/winfsp/hubfs/blob/master/Contributors.asciidoc) you do not need to do so again. 8 | - [ ] **Topic branch**: Avoid creating the PR off the master branch of your fork. Consider creating a topic branch and request a pull from that. This allows you to add commits to the master branch of your fork without affecting this PR. 9 | - [ ] **Style**: Follow the same code style as the rest of the project. 10 | - [ ] **Tests**: Include tests to the extent that it is possible, especially if you add a new feature. 11 | - [ ] **Quality**: Your design and code should be of high quality and something that you are proud of. 12 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - "*" 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | jobs: 11 | 12 | build: 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | os: [windows-latest, ubuntu-latest, macos-latest] 17 | fail-fast: false 18 | 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v2 22 | with: 23 | submodules: recursive 24 | 25 | - name: Install go 26 | uses: actions/setup-go@v2 27 | with: 28 | stable: true 29 | go-version: 1.18.* 30 | 31 | - name: Install winfsp and winfsp-tests (Windows) 32 | if: runner.os == 'Windows' 33 | run: | 34 | $releases = Invoke-WebRequest https://api.github.com/repos/winfsp/winfsp/releases | ` 35 | ConvertFrom-Json 36 | 37 | $asseturi = $releases[0].assets.browser_download_url | ` 38 | Where-Object { $_ -match "winfsp-.*\.msi" } 39 | Invoke-WebRequest -Uri $asseturi -Out winfsp.msi 40 | Start-Process -NoNewWindow -Wait msiexec "/i winfsp.msi /qn" 41 | 42 | - name: Install FUSE and secfs.test (Linux) 43 | if: runner.os == 'Linux' 44 | run: | 45 | sudo apt-get -qq install libfuse-dev 46 | sudo apt-get -qq install libacl1-dev 47 | 48 | - name: Install FUSE and secfs.test (macOS) 49 | if: runner.os == 'macOS' 50 | run: | 51 | # requires macos-10.15; does not work on macos-latest 52 | # see https://github.com/actions/virtual-environments/issues/4731 53 | brew install macfuse 54 | 55 | - name: Build HUBFS 56 | env: 57 | GOARCHLIST: amd64 arm64 58 | run: | 59 | build/make dist 60 | 61 | - name: Upload redistributables 62 | uses: actions/upload-artifact@v2 63 | with: 64 | name: Redistributables 65 | path: | 66 | build/out/hubfs-*-*.msi 67 | build/out/hubfs-*-*.zip 68 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - "*" 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | jobs: 11 | 12 | test: 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | os: [windows-latest, ubuntu-latest, macos-10.15] 17 | fail-fast: false 18 | 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v2 22 | with: 23 | submodules: recursive 24 | 25 | - name: Install go 26 | uses: actions/setup-go@v2 27 | with: 28 | stable: true 29 | go-version: 1.18.* 30 | 31 | - name: Install winfsp and winfsp-tests (Windows) 32 | if: runner.os == 'Windows' 33 | run: | 34 | $releases = Invoke-WebRequest https://api.github.com/repos/winfsp/winfsp/releases | ` 35 | ConvertFrom-Json 36 | 37 | $asseturi = $releases[0].assets.browser_download_url | ` 38 | Where-Object { $_ -match "winfsp-.*\.msi" } 39 | Invoke-WebRequest -Uri $asseturi -Out winfsp.msi 40 | Start-Process -NoNewWindow -Wait msiexec "/i winfsp.msi /qn" 41 | 42 | $asseturi = $releases[0].assets.browser_download_url | ` 43 | Where-Object { $_ -match "winfsp-tests-.*\.zip" } 44 | Invoke-WebRequest -Uri $asseturi -Out winfsp-tests.zip 45 | Expand-Archive -Path winfsp-tests.zip 46 | Copy-Item "C:\Program Files (x86)\WinFsp\bin\winfsp-x64.dll" winfsp-tests 47 | 48 | - name: Install FUSE and secfs.test (Linux) 49 | if: runner.os == 'Linux' 50 | run: | 51 | sudo apt-get -qq install libfuse-dev 52 | sudo apt-get -qq install libacl1-dev 53 | 54 | git clone -q https://github.com/billziss-gh/secfs.test.git secfs.test 55 | git -C secfs.test checkout -q edf5eb4a108bfb41073f765aef0cdd32bb3ee1ed 56 | mkdir -p secfs.test/tools/bin 57 | touch secfs.test/tools/bin/bonnie++ 58 | touch secfs.test/tools/bin/iozone 59 | make -C secfs.test 60 | 61 | # configure fstest for cgofuse 62 | sed -e 's/^fs=.*$/fs="cgofuse"/' -i"" secfs.test/fstest/fstest/tests/conf 63 | 64 | # remove irrelevant tests 65 | rm -rf secfs.test/fstest/fstest/tests/xacl 66 | rm -rf secfs.test/fstest/fstest/tests/zzz_ResourceFork 67 | 68 | - name: Install FUSE and secfs.test (macOS) 69 | if: runner.os == 'macOS' 70 | run: | 71 | # requires macos-10.15; does not work on macos-latest 72 | # see https://github.com/actions/virtual-environments/issues/4731 73 | brew install macfuse 74 | 75 | git clone -q https://github.com/billziss-gh/secfs.test.git secfs.test 76 | git -C secfs.test checkout -q edf5eb4a108bfb41073f765aef0cdd32bb3ee1ed 77 | mkdir -p secfs.test/tools/bin 78 | touch secfs.test/tools/bin/bonnie++ 79 | touch secfs.test/tools/bin/iozone 80 | make -C secfs.test 81 | 82 | # configure fstest for cgofuse 83 | sed -e 's/^fs=.*$/fs="cgofuse"/' -i "" secfs.test/fstest/fstest/tests/conf 84 | 85 | # monkey patch some tests for macOS 86 | sed -e 's/expect EINVAL \(.*\.\.\)$/expect ENOTEMPTY \1/' -i "" secfs.test/fstest/fstest/tests/rmdir/12.t 87 | sed -e 's/lchmod)/lchmod) return 1/' -i "" secfs.test/fstest/fstest/tests/misc.sh 88 | 89 | # remove irrelevant tests 90 | rm -rf secfs.test/fstest/fstest/tests/xacl 91 | rm -rf secfs.test/fstest/fstest/tests/zzz_ResourceFork 92 | 93 | # remove tests that fail on macOS with ENAMETOOLONG: these tests send a path 94 | # with a length close to 1024; when ptfs/unionfs prefixes them with the backing 95 | # directory the total path is over 1024 and macOS errors with ENAMETOOLONG 96 | rm secfs.test/fstest/fstest/tests/chflags/03.t 97 | rm secfs.test/fstest/fstest/tests/chmod/03.t 98 | rm secfs.test/fstest/fstest/tests/chown/03.t 99 | rm secfs.test/fstest/fstest/tests/link/03.t 100 | rm secfs.test/fstest/fstest/tests/mkdir/03.t 101 | rm secfs.test/fstest/fstest/tests/mkfifo/03.t 102 | rm secfs.test/fstest/fstest/tests/open/03.t 103 | rm secfs.test/fstest/fstest/tests/rename/02.t 104 | rm secfs.test/fstest/fstest/tests/rmdir/03.t 105 | rm secfs.test/fstest/fstest/tests/symlink/03.t 106 | rm secfs.test/fstest/fstest/tests/truncate/03.t 107 | rm secfs.test/fstest/fstest/tests/unlink/03.t 108 | 109 | - name: Build HUBFS 110 | run: | 111 | build/make dist 112 | 113 | - name: Test HUBFS packages (Windows) 114 | if: runner.os == 'Windows' 115 | env: 116 | HUBFS_TOKEN: ${{ secrets.HUBFS_TOKEN }} 117 | run: | 118 | Set-Location src 119 | $env:CGO_ENABLED=0 120 | go test -count=1 ./... 121 | 122 | - name: Test HUBFS packages (Linux / macOS) 123 | if: runner.os == 'Linux' || runner.os == 'macOS' 124 | env: 125 | HUBFS_TOKEN: ${{ secrets.HUBFS_TOKEN }} 126 | run: | 127 | cd src 128 | go test -count=1 ./... 129 | 130 | - name: Test component file systems (Windows) 131 | if: runner.os == 'Windows' 132 | run: | 133 | Set-PSDebug -Trace 1 134 | 135 | $testexe = (Get-Item winfsp-tests\winfsp-tests-x64.exe) 136 | Set-Location src 137 | New-Item -Type Directory -Path hi,lo >$null 138 | $env:CGO_ENABLED=0 139 | 140 | go build _tools/ptfs.go 141 | go build _tools/unionfs.go 142 | 143 | Start-Process -NoNewWindow .\ptfs.exe "-o uid=-1,rellinks,FileInfoTimeout=-1 lo X:" 144 | Start-Sleep 3 145 | Push-Location X:\ 146 | . $testexe --fuse-external --resilient --case-insensitive-cmp ` 147 | +* ` 148 | -create_fileattr_test ` 149 | -delete_access_test ` 150 | -delete_ex_test ` 151 | -create_backup_test ` 152 | -create_restore_test ` 153 | -rename_flipflop_test ` 154 | -exec_rename_dir_test ` 155 | -reparse_nfs_test ` 156 | -ea* 157 | Stop-Process -Name ptfs 158 | Start-Sleep 3 159 | Pop-Location 160 | 161 | Start-Process -NoNewWindow .\unionfs.exe "-o uid=-1,rellinks,FileInfoTimeout=-1 hi lo X:" 162 | Start-Sleep 3 163 | Push-Location X:\ 164 | . $testexe --fuse-external --resilient --case-insensitive-cmp ` 165 | +* ` 166 | -create_fileattr_test ` 167 | -delete_access_test ` 168 | -delete_ex_test ` 169 | -create_backup_test ` 170 | -create_restore_test ` 171 | -rename_flipflop_test ` 172 | -exec_rename_dir_test ` 173 | -reparse_nfs_test ` 174 | -ea* 175 | Stop-Process -Name unionfs 176 | Start-Sleep 3 177 | Pop-Location 178 | 179 | - name: Test component file systems (Linux / macOS) 180 | if: runner.os == 'Linux' || runner.os == 'macOS' 181 | run: | 182 | set -x 183 | 184 | cd src 185 | mkdir hi lo mnt 186 | 187 | go build _tools/ptfs.go 188 | go build _tools/unionfs.go 189 | 190 | sudo ./ptfs -o allow_other,default_permissions,use_ino,attr_timeout=0 lo mnt & 191 | sleep 3 192 | (cd mnt && sudo prove -fr ../../secfs.test/fstest/fstest/tests) 193 | sudo umount mnt 194 | 195 | sudo ./unionfs -o allow_other,default_permissions,use_ino,attr_timeout=0 hi lo mnt & 196 | sleep 3 197 | (cd mnt && sudo prove -fr ../../secfs.test/fstest/fstest/tests) 198 | (cd mnt && ../../secfs.test/tools/bin/fsx -N 50000 test xxxxxx) 199 | seed=$(date +%s) 200 | ../secfs.test/tools/bin/fsstress -d mnt -s $seed -n 5000 -p 10 -S 201 | ../secfs.test/tools/bin/fsstress -d mnt -s $seed -n 5000 -p 10 -S 202 | sudo umount mnt 203 | 204 | rm -rf hi lo 205 | rmdir mnt 206 | 207 | - name: Test HUBFS file system (Windows) 208 | if: runner.os == 'Windows' 209 | env: 210 | HUBFS_TOKEN: ${{ secrets.HUBFS_TOKEN }} 211 | HUBFS_GL_TOKEN: ${{ secrets.HUBFS_GL_TOKEN }} 212 | run: | 213 | Set-PSDebug -Trace 1 214 | 215 | Start-Process -NoNewWindow .\build\out\hubfs.exe "-auth token=$env:HUBFS_TOKEN github.com X:" 216 | Start-Sleep 3 217 | Push-Location X:\winfsp\hubfs\master 218 | build/make dist 219 | cd X:\winfsp\hubfs\TestTag 220 | build/make dist 221 | Stop-Process -Name hubfs 222 | Start-Sleep 3 223 | Pop-Location 224 | 225 | Start-Process -NoNewWindow .\build\out\hubfs.exe "-auth token=$env:HUBFS_GL_TOKEN gitlab.com X:" 226 | Start-Sleep 3 227 | Push-Location X:\winfsp\hubfs\master 228 | build/make dist 229 | cd X:\winfsp\hubfs\TestTag 230 | build/make dist 231 | Stop-Process -Name hubfs 232 | Start-Sleep 3 233 | Pop-Location 234 | 235 | - name: Test HUBFS file system (Linux / macOS) 236 | if: runner.os == 'Linux' || runner.os == 'macOS' 237 | env: 238 | HUBFS_TOKEN: ${{ secrets.HUBFS_TOKEN }} 239 | HUBFS_GL_TOKEN: ${{ secrets.HUBFS_GL_TOKEN }} 240 | run: | 241 | set -x 242 | 243 | mkdir mnt 244 | 245 | ./build/out/hubfs -auth token=$HUBFS_TOKEN github.com mnt & 246 | sleep 3 247 | (cd mnt/winfsp/hubfs/master && build/make dist) 248 | (cd mnt/winfsp/hubfs/TestTag && build/make dist) 249 | sudo umount mnt 250 | 251 | ./build/out/hubfs -auth token=$HUBFS_GL_TOKEN gitlab.com mnt & 252 | sleep 3 253 | (cd mnt/winfsp/hubfs/master && build/make dist) 254 | (cd mnt/winfsp/hubfs/TestTag && build/make dist) 255 | sudo umount mnt 256 | 257 | rmdir mnt 258 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build/out 2 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "ext/glibc-compat"] 2 | path = ext/glibc-compat 3 | url = https://github.com/billziss-gh/glibc-compat 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "HUBFS", 6 | "type": "go", 7 | "request": "launch", 8 | "mode": "auto", 9 | "program": "${workspaceFolder}/src", 10 | "args": ["X:"], 11 | "env": {"CGO_ENABLED": "0"} 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "go.buildFlags": [ 3 | "-tags=ent" 4 | ], 5 | "go.toolsEnvVars": { 6 | "CGO_ENABLED": "0" 7 | } 8 | } -------------------------------------------------------------------------------- /Contributors.asciidoc: -------------------------------------------------------------------------------- 1 | CONTRIBUTORS 2 | ============ 3 | 4 | This document contains a list of all contributors to this project. Contributors that submit changes to this project MUST also change this document to identify themselves by adding their name and email address to the CONTRIBUTOR LIST. The CONTRIBUTOR LIST is maintained in alphabetical order. 5 | 6 | A contributor who adds themselves to this list indicates their acceptance of the terms of the CONTRIBUTOR AGREEMENT below. There are NO EXCEPTIONS to this rule. If you do not wish to accept the CONTRIBUTOR AGREEMENT, please do not submit any changes. 7 | 8 | This CONTRIBUTOR AGREEMENT is based on the Oracle Contributor Agreement and is used under the Creative Commons Attribution-Share Alike 3.0 Unported License. The original agreement and an FAQ can be found at this location: http://www.oracle.com/technetwork/community/oca-486395.html 9 | 10 | This document uses the asciidoc format: http://asciidoc.org. 11 | 12 | 13 | CONTRIBUTOR AGREEMENT 14 | --------------------- 15 | 16 | This CONTRIBUTOR AGREEMENT applies to any contribution that you make to the HUBFS project (the "project"), and sets out the intellectual property rights you grant to us in the contributed materials. The term “us” shall mean the original author of this project: Bill Zissimopoulos . The term “you” shall mean the persons or entities identified below. If you agree to be bound by these terms, add your name and email address to the CONTRIBUTOR LIST below; this action will constitute signing this CONTRIBUTOR AGREEMENT. These terms and conditions constitute a binding legal agreement. 17 | 18 | 1. The term 'contribution' or ‘contributed materials’ means any source code, object code, patch, tool, sample, graphic, specification, manual, documentation, or any other material posted or submitted by you to the project. 19 | 20 | 2. With respect to any worldwide copyrights, or copyright applications and registrations, in your contribution: 21 | * you hereby assign to us joint ownership, and to the extent that such assignment is or becomes invalid, ineffective or unenforceable, you hereby grant to us a perpetual, irrevocable, non-exclusive, worldwide, no-charge, royalty-free, unrestricted license to exercise all rights under those copyrights. This includes, at our option, the right to sublicense these same rights to third parties through multiple levels of sublicensees or other licensing arrangements; 22 | * you agree that each of us can do all things in relation to your contribution as if each of us were the sole owners, and if one of us makes a derivative work of your contribution, the one who makes the derivative work (or has it made) will be the sole owner of that derivative work; 23 | * you agree that you will not assert any moral rights in your contribution against us, our licensees or transferees; 24 | * you agree that we may register a copyright in your contribution and exercise all ownership rights associated with it; and 25 | * you agree that neither of us has any duty to consult with, obtain the consent of, pay or render an accounting to the other for any use or distribution of your contribution. 26 | 27 | 3. With respect to any patents you own, or that you can license without payment to any third party, you hereby grant to us a perpetual, irrevocable, non-exclusive, worldwide, no-charge, royalty-free license to: 28 | * make, have made, use, sell, offer to sell, import, and otherwise transfer your contribution in whole or in part, alone or in combination with or included in any product, work or materials arising out of the project to which your contribution was submitted, and 29 | * at our option, to sublicense these same rights to third parties through multiple levels of sublicensees or other licensing arrangements. 30 | 31 | 4. Except as set out above, you keep all right, title, and interest in your contribution. The rights that you grant to us under these terms are effective on the date you first submitted a contribution to us, even if your submission took place before the date you sign these terms. Any contribution we make available under any license will also be made available under a suitable FSF (Free Software Foundation) or OSI (Open Source Initiative) approved license. 32 | 33 | 5. You covenant, represent, warrant and agree that: 34 | * each contribution that you submit is and shall be an original work of authorship and you can legally grant the rights set out in this CONTRIBUTOR AGREEMENT; 35 | * to the best of your knowledge, each contribution will not violate any third party's copyrights, trademarks, patents, or other intellectual property rights; and 36 | * each contribution shall be in compliance with U.S. export control laws and other applicable export and import laws. 37 | * You agree to notify us if you become aware of any circumstance which would make any of the foregoing representations inaccurate in any respect. 38 | 39 | 6. This CONTRIBUTOR AGREEMENT is governed by the laws of the State of Washington and applicable U.S. Federal law. Any choice of law rules will not apply. 40 | 41 | 7. Please add your name and email address in the CONTRIBUTOR LIST below: 42 | * Either "I am signing on behalf of myself as an individual and no other person or entity, including my employer, has or will have rights with respect my contributions". In this case add your name using the following format: 43 | + 44 | ---- 45 | |First Last |name at example.com 46 | ---- 47 | * Or "I am signing on behalf of my employer or a legal entity and I have the actual authority to contractually bind that entity". In this case add your name using the following format: 48 | + 49 | ---- 50 | |First Last (Company and/or URL) |name at example.com 51 | ---- 52 | 53 | 54 | CONTRIBUTOR LIST 55 | ---------------- 56 | |=== 57 | |Bill Zissimopoulos |billziss at navimatics.com 58 | |=== 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |
4 |
5 | HUBFS · File System for GitHub 6 |

7 | 8 |

9 | 10 | 11 | 12 | 13 | HUBFS is a file system for GitHub and Git. Git repositories and their contents are represented as regular directories and files and are accessible by any application, without the application having any knowledge that it is really accessing a remote Git repository. The repositories are writable and allow editing files and running build operations. 14 |
15 |
16 | 17 |

18 | 19 | ## How to use 20 | 21 | HUBFS is a command-line program with a usage documented below. On Windows there is also good desktop integration so that you can use HUBFS without the command line. 22 | 23 | HUBFS supports both authenticated and non-authenticated access to repositories. When using HUBFS without authentication, only public repositories are available. When using HUBFS with authentication, both public and private repositories become available; an additional benefit is that the rate limiting that GitHub does for certain operations is relaxed. 24 | 25 | In order to mount HUBFS issue the command `hubfs MOUNTPOINT`. For example, `hubfs H:` on Windows or `hubfs mnt` on macOS and Linux. 26 | 27 | The first time you run HUBFS you will be prompted to authorize it with GitHub: 28 | 29 | ``` 30 | > ./hubfs H: 31 | First, copy your one-time code: XXXX-XXXX 32 | Then press [Enter] to continue in the web browser... 33 | ``` 34 | 35 | HUBFS will then open your system browser where you will be able to authorize it with GitHub. HUBFS will store the resulting authorization token in the system keyring (Windows Credential Manager, macOS Keychain, etc.). Subsequent runs of HUBFS will use the authorization token from the system keyring and you will not be required to re-authorize the application. 36 | 37 | To unmount the file system simply use Ctrl-C. On macOS and Linux you may also be able to unmount using `umount` or `fusermount -u`. 38 | 39 | ### Full command-line usage 40 | 41 | The full HUBFS command line usage is as follows: 42 | 43 | ``` 44 | usage: hubfs [options] [remote] mountpoint 45 | 46 | -auth method 47 | method is from list below; auth tokens are stored in system keyring 48 | - force perform interactive auth even if token present 49 | - full perform interactive auth if token not present (default) 50 | - required auth token required to be present 51 | - optional auth token will be used if present 52 | - none do not use auth token even if present 53 | - token=T use specified auth token T; do not use system keyring 54 | -authkey name 55 | name of key that stores auth token in system keyring 56 | -authonly 57 | perform auth only; do not mount 58 | -d debug output 59 | -filter rules 60 | list of rules that determine repo availability 61 | - list form: rule1,rule2,... 62 | - rule form: [+-]owner or [+-]owner/repo 63 | - rule is include (+) or exclude (-) (default: include) 64 | - rule owner/repo can use wildcards for pattern matching 65 | -o options 66 | FUSE mount options 67 | (default: uid=-1,gid=-1,rellinks,FileInfoTimeout=-1) 68 | -version 69 | print version information 70 | ``` 71 | 72 | (The default FUSE mount options depend on the OS. The `uid=-1,gid=-1` option specifies that the owner/group of HUBFS files is determined by the user/group that launches the file system. This works on Windows, Linux and macOS.) 73 | 74 | ### File system representation 75 | 76 | By default HUBFS presents the following file system hierarchy: / *owner* / *repository* / *ref* / *path* 77 | 78 | - *Owner* represents the owner of repositories under GitHub. It may be a user or organization. An *owner* is presented as a subdirectory of the root directory and contains *repositories*. However the root directory cannot be listed, because there are far too many owners to list. 79 | 80 | - *Repository* represents a repository owned by an *owner*. A *repository* is presented as a directory that contains *refs*. 81 | 82 | - *Ref* represents a git "ref". It may be a git branch, a git tag or even a commit hash. A *ref* is presented as a directory that contains repository content. However when listing a *repository* directory only branch *refs* are listed. 83 | 84 | - *Path* is a path to actual file content within the repository. 85 | 86 | HUBFS interprets submodules as symlinks. These submodules can be followed if they point to other GitHub repositories. General repository symlinks should work as well. (On Windows you must use the FUSE option `rellinks` for this to work correctly.) 87 | 88 | With release 2022 Beta1 HUBFS *ref* directories are now writable. This is implemented as a union file system that overlays a read-write local file system over the read-only Git content. This scheme allows files to be edited and builds to be performed. A special file named `.keep` is created at the *ref* root (full path: / *owner* / *repository* / *ref* / `.keep`). When the edit/build modifications are no longer required the `.keep` file may be deleted and the *ref* root will be garbage collected when not in use (i.e. when no files are open in it -- having a terminal window open with a current directory inside a *ref* root counts as an open file and the *ref* will not be garbage collected). 89 | 90 | ### Windows integration 91 | 92 | When you use the MSI installer under Windows there is better integration of HUBFS with the rest of the system: 93 | 94 | - There is a "Start Menu > HUBFS > Perform GitHub auth" shortcut that allows you to authorize HUBFS with GitHub without using the command line. 95 | 96 | - You can mount HUBFS drives using the Windows Explorer "Map Network Drive" functionality. To dismount use the "Disconnect Network Drive" functionality. (It is recommended to first authorize HUBFS with GitHub using the above mentioned shortcut.) 97 | 98 | 99 | 100 | - You can also mount HUBFS with the `net use` command. The command `net use H: \\hubfs\github.com` will mount HUBFS as drive `H:`. The command `net use H: /delete` will dismount the `H:` drive. 101 | 102 | ## How to build 103 | 104 | In order to build HUBFS run `build/make`. The build prerequisites for individual platforms are listed below: 105 | 106 | - Windows: [Go 1.16](https://golang.org/dl/), [WinFsp](https://github.com/winfsp/winfsp), gcc (e.g. from [Mingw-builds](http://mingw-w64.org/doku.php/download)) 107 | 108 | - macOS: [Go 1.16](https://golang.org/dl/), [FUSE for macOS](https://osxfuse.github.io), [command line tools](https://developer.apple.com/library/content/technotes/tn2339/_index.html) 109 | 110 | - Linux: Prerequisites: [Go 1.16](https://golang.org/dl/), libfuse-dev, gcc 111 | 112 | ## How it works 113 | 114 | HUBFS is a cross-platform file system written in Go. Under the hood it uses [cgofuse](https://github.com/winfsp/cgofuse) over either [WinFsp](https://github.com/winfsp/winfsp) on Windows, [macFUSE](https://osxfuse.github.io/) on macOS or [libfuse](https://github.com/libfuse/libfuse/) on Linux. It also uses [go-git](https://github.com/go-git/go-git) for some git functionality. 115 | 116 | HUBFS interfaces with GitHub using the [REST API](https://docs.github.com/en/rest). The REST API is used to discover owners and repositories in the file system hierarchy, but is not used to access repository content. The REST API is rate limited ([details](https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting)). 117 | 118 | HUBFS uses the git [pack protocol](https://git-scm.com/docs/pack-protocol) to access repository content. This is the same protocol that git uses during operations like `git clone`. HUBFS uses some of the newer capabilities of the pack protocol that allow it to fetch content on demand. HUBFS does not have to see all of the repository history or download all of the repository content. It will only download the commits, trees and blobs necessary to back the directories and files that the user is interested in. Note that the git pack protocol is not rate limited. 119 | 120 | HUBFS caches information in memory and on local disk to avoid the need to contact the servers too often. 121 | 122 | ### Git pack protocol use 123 | 124 | HUBFS uses the git pack protocol to fetch repository refs and objects. When HUBFS first connects to the Git server it fetches all of the server's advertised refs. HUBFS exposes these refs as subdirectories of a repository. 125 | 126 | When accessing the content of a ref for the first time, the commit object pointed by the ref is fetched, then the tree object pointed by the commit is fetched. When fetching a tree HUBFS will also fetch all blobs directly referenced by the tree, this is required to compute proper `stat` data (esp. size) for files. 127 | 128 | HUBFS fetches objects with a depth of 1 and a filter of `tree:0`. This ensures that the git server will only send objects whose hashes have been explicitly requested. This avoids sending extraneous information and speeds up communication with the server. 129 | 130 | ## Security issues 131 | 132 | - Consider a program that accesses files under `/COMMON-NAME/DIR`. The owner of the `COMMON-NAME` GitHub account could create a repository named `DIR` and inject arbitrary file content into the program's process. This problem is particularly important when mounting the file system as a drive on Windows. To fix this problem: 133 | 134 | - Run HUBFS with the `-filter` option. For example, running HUBFS with `-filter ORG` will make available the repositories in `ORG` only. Files within the file system will be accessible as / `ORG` / *repository* / *ref* / *path*. (The `-filter` option allows the inclusion/exclusion of multiple repositories and supports wildcard syntax. Please see the HUBFS usage for more.) 135 | 136 | - Run HUBFS with a "prefix". For example, the (Windows) command line `./hubfs github.com/ORG H:` will place the root of the file system within `ORG` and thus make available the repositories in `ORG` only. Files within the file system will be accessible as / *repository* / *ref* / *path*. 137 | 138 | ## Potential future improvements 139 | 140 | - The file system does not present a `.git` subdirectory. It may be worthwhile to present a virtual `.git` directory so that simple Git commands (like `git status`) would work. 141 | 142 | - Additional providers such as GitHub Enterprise, BitBucket, GitLab, etc. 143 | 144 | ## License 145 | 146 | This project is licensed under the [GNU Affero General Public License version 3](License.txt). -------------------------------------------------------------------------------- /art/hubfs-glow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winfsp/hubfs/c55f351edacee53d6443519375247f21e91dc9a4/art/hubfs-glow.png -------------------------------------------------------------------------------- /art/hubfs.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winfsp/hubfs/c55f351edacee53d6443519375247f21e91dc9a4/art/hubfs.afdesign -------------------------------------------------------------------------------- /art/hubfs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winfsp/hubfs/c55f351edacee53d6443519375247f21e91dc9a4/art/hubfs.png -------------------------------------------------------------------------------- /art/wixbanner.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winfsp/hubfs/c55f351edacee53d6443519375247f21e91dc9a4/art/wixbanner.afdesign -------------------------------------------------------------------------------- /art/wixbanner.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winfsp/hubfs/c55f351edacee53d6443519375247f21e91dc9a4/art/wixbanner.bmp -------------------------------------------------------------------------------- /art/wixdialog-Beta.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winfsp/hubfs/c55f351edacee53d6443519375247f21e91dc9a4/art/wixdialog-Beta.bmp -------------------------------------------------------------------------------- /art/wixdialog-Gold.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winfsp/hubfs/c55f351edacee53d6443519375247f21e91dc9a4/art/wixdialog-Gold.bmp -------------------------------------------------------------------------------- /art/wixdialog.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winfsp/hubfs/c55f351edacee53d6443519375247f21e91dc9a4/art/wixdialog.afdesign -------------------------------------------------------------------------------- /build/DigiCert High Assurance EV Root CA.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFOzCCAyOgAwIBAgIKYSBNtAAAAAAAJzANBgkqhkiG9w0BAQUFADB/MQswCQYD 3 | VQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEe 4 | MBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSkwJwYDVQQDEyBNaWNyb3Nv 5 | ZnQgQ29kZSBWZXJpZmljYXRpb24gUm9vdDAeFw0xMTA0MTUxOTQ1MzNaFw0yMTA0 6 | MTUxOTU1MzNaMGwxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx 7 | GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xKzApBgNVBAMTIkRpZ2lDZXJ0IEhp 8 | Z2ggQXNzdXJhbmNlIEVWIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw 9 | ggEKAoIBAQDGzOVz5vvUu+UtLTKm3+WBP8nNJUm2cSrD1ZQ0Z6IKHLBfaaZAscS3 10 | so/QmKSpQVk609yU1jzbdDikSsxNJYL3SqVTEjju80ltcZF+Y7arpl/DpIT4T2JR 11 | vvjF7Ns4kuMG5QiRDMQoQVX7y1qJFX5x6DW/TXIJPb46OFBbdzEbjbPHJEWap6xt 12 | ABRaBLe6E+tRCphBQSJOZWGHgUFQpnlcid4ZSlfVLuZdHFMsfpjNGgYWpGhz0DQE 13 | E1yhcdNafFXbXmThN4cwVgTlEbQpgBLxeTmIogIRfCdmt4i3ePLKCqg4qwpkwr9m 14 | XZWEwaElHoddGlALIBLMQbtuC1E4uEvLAgMBAAGjgcswgcgwEQYDVR0gBAowCDAG 15 | BgRVHSAAMAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSx 16 | PsNpA/i/RwHUmCYaCALvY2QrwzAfBgNVHSMEGDAWgBRi+wohW39DbhHaCVRQa/XS 17 | lnHxnjBVBgNVHR8ETjBMMEqgSKBGhkRodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20v 18 | cGtpL2NybC9wcm9kdWN0cy9NaWNyb3NvZnRDb2RlVmVyaWZSb290LmNybDANBgkq 19 | hkiG9w0BAQUFAAOCAgEAIIzBWe1vnGstwUo+dR1FTEFQHL2A6tmwkosGKhM/Uxae 20 | VjlqimO2eCR59X24uUehCpbC9su9omafBuGs0nkJDv083KwCDHCvPxvseH7U60sF 21 | YCbZc2GRIe2waGPglxKrb6AS7dmf0tonPLPkVvnR1IEPcb1CfKaJ3M3VvZWiq/GT 22 | EX3orDEpqF1mcEGd/HXJ1bMaOSrQhQVQi6yRysSTy3GlnaSUb1gM+m4gxAgxtYWd 23 | foH50j3KWxiFbAqG7CIJG6V0NE9/KLyVSqsdtpiwXQmkd3Z+76eOXYT2GCTL0W2m 24 | w6GcwhB1gP+dMv3mz0M6gvfOj+FyKptit1/tlRo5XC+UbUi3AV8zL7vcLXM0iQRC 25 | ChyLefmj+hfv+qEaEN/gssGV61wMBZc7NT4YiE3bbL8kiY3Ivdifezk6JKDV39Hz 26 | ShqX9qZveh+wkKmzrAE5kdNht2TxPlc4A6/OetK1kPWu3DmZ1bY8l+2myxbHfWsq 27 | TJCU5kxU/R7NIOzOaJyHWOlhYL7rDsnVGX2f6Xi9DqwhdQePqW7gjGoqa5zj52W8 28 | vC08bdwE3GdFNjKvBIG8qABuYUyVxVzUjo6fL8EydL29EWUDB83vt14CV9qG1Boo 29 | NK+ISbLPpd2CVm9oqhTiWVT+/+ru7+qScCJggeMlI8CfzA9JsjWqWMM6w9kWlBA= 30 | -----END CERTIFICATE----- 31 | -------------------------------------------------------------------------------- /build/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Hubfs 2 | # 3 | # Copyright 2021-2022 Bill Zissimopoulos 4 | # 5 | # This file is part of Hubfs. 6 | # 7 | # You can redistribute it and/or modify it under the terms of the GNU 8 | # Affero General Public License version 3 as published by the Free 9 | # Software Foundation. 10 | 11 | MyProductName = "HUBFS" 12 | MyDescription = "File system for GitHub" 13 | MyCopyright = "2021-2022 Bill Zissimopoulos" 14 | MyCompanyName = "Navimatics LLC" 15 | MyCertIssuer = "DigiCert" 16 | MyCrossCert = "DigiCert High Assurance EV Root CA.crt" 17 | MyBuildNumber = $(shell date +%y%j) 18 | MyVersion = 1.0.$(MyBuildNumber) 19 | MyProductVersion = "2022 Beta2" 20 | MyProductStage = "Beta" 21 | 22 | ifeq ($(TAG),) 23 | MyProductTag = "" 24 | else ifeq ($(TAG),pro) 25 | MyProductTag = "Professional" 26 | else ifeq ($(TAG),ent) 27 | MyProductTag = "Enterprise" 28 | else 29 | $(error invalid TAG; possible options: pro ent) 30 | endif 31 | 32 | BldDir = $(dir $(realpath $(lastword $(MAKEFILE_LIST)))) 33 | SrcDir = $(BldDir)../src/ 34 | ExtDir = $(BldDir)../ext/ 35 | OutDir = $(BldDir)out/ 36 | 37 | ifneq ($(OS),Windows_NT) 38 | OS = $(shell uname) 39 | endif 40 | 41 | GoBuild = go build 42 | ExeSuffix = 43 | ifeq ($(OS),Windows_NT) 44 | ExeSuffix = .exe 45 | else ifeq ($(OS),Linux) 46 | export CGO_CFLAGS=-include $(ExtDir)glibc-compat/glibc-2.17.h 47 | else ifeq ($(OS),Darwin) 48 | GoBuild = $(BldDir)gobuild.mac 49 | endif 50 | 51 | GoBuildTags = 52 | TagInfix = 53 | ifdef TAG 54 | GoBuildTags = -tags $(TAG) 55 | TagInfix = -$(TAG) 56 | endif 57 | 58 | .PHONY: default 59 | default: build 60 | 61 | .PHONY: build 62 | build: 63 | cd $(SrcDir) && \ 64 | $(GoBuild) \ 65 | $(GoBuildTags) \ 66 | -buildvcs=false \ 67 | -trimpath \ 68 | -ldflags "-s -w \ 69 | -X \"main.MyProductName=$(subst $\",,$(MyProductName))\" \ 70 | -X \"main.MyDescription=$(subst $\",,$(MyDescription))\" \ 71 | -X \"main.MyCopyright=$(subst $\",,$(MyCopyright))\" \ 72 | -X \"main.MyVersion=$(subst $\",,$(MyVersion))\" \ 73 | -X \"main.MyProductVersion=$(subst $\",,$(MyProductVersion))\" \ 74 | -X \"main.MyProductTag=$(subst $\",,$(MyProductTag))\" \ 75 | " \ 76 | -o $(OutDir)hubfs$(ExeSuffix) 77 | 78 | .PHONY: racy 79 | racy: 80 | cd $(SrcDir) && \ 81 | go build $(GoBuildTags) -race -o $(OutDir)hubfs$(ExeSuffix) 82 | 83 | .PHONY: test 84 | test: 85 | cd $(SrcDir) && \ 86 | go test $(GoBuildTags) -count=1 ./... 87 | 88 | .PHONY: dist 89 | dist: build 90 | ifeq ($(OS),Windows_NT) 91 | powershell -NoProfile -NonInteractive -ExecutionPolicy Unrestricted \ 92 | "Compress-Archive -Force -Path $(OutDir)hubfs.exe -DestinationPath $(OutDir)hubfs$(TagInfix)-win-$(MyVersion).zip" 93 | candle -nologo -arch x64 -pedantic \ 94 | -dMyProductName=$(MyProductName) \ 95 | -dMyProductTag=$(MyProductTag) \ 96 | -dMyDescription=$(MyDescription) \ 97 | -dMyCompanyName=$(MyCompanyName) \ 98 | -dMyVersion=$(MyVersion) \ 99 | -dMyProductVersion=$(MyProductVersion) \ 100 | -dMyProductStage=$(MyProductStage) \ 101 | -o $(OutDir)hubfs$(TagInfix).wixobj \ 102 | $(BldDir)hubfs.wxs 103 | light -nologo \ 104 | -ext WixUIExtension \ 105 | -sice:ICE61 \ 106 | -spdb \ 107 | -o $(OutDir)hubfs$(TagInfix)-win-$(MyVersion).msi \ 108 | $(OutDir)hubfs$(TagInfix).wixobj 109 | signtool sign \ 110 | /ac $(MyCrossCert) \ 111 | /i $(MyCertIssuer) \ 112 | /n $(MyCompanyName) \ 113 | /d $(MyDescription) \ 114 | /fd sha1 \ 115 | /t http://timestamp.digicert.com \ 116 | $(OutDir)hubfs$(TagInfix)-win-$(MyVersion).msi || \ 117 | echo "SIGNING FAILED! The product has been successfully built, but not signed." 1>&2 118 | endif 119 | ifeq ($(OS),Linux) 120 | rm -f $(OutDir)hubfs$(TagInfix)-lnx-$(MyVersion).zip 121 | zip $(OutDir)hubfs$(TagInfix)-lnx-$(MyVersion).zip $(OutDir)hubfs 122 | endif 123 | ifeq ($(OS),Darwin) 124 | rm -f $(OutDir)hubfs$(TagInfix)-mac-$(MyVersion).zip 125 | zip $(OutDir)hubfs$(TagInfix)-mac-$(MyVersion).zip $(OutDir)hubfs 126 | endif 127 | -------------------------------------------------------------------------------- /build/gobuild.mac: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | for target; do true; done 6 | 7 | if [[ -n $GOARCHLIST ]]; then 8 | targetlist= 9 | for arch in $GOARCHLIST; do 10 | echo GOOS=darwin GOARCH=$arch CGO_ENABLED=1 go build $@ 11 | GOOS=darwin GOARCH=$arch CGO_ENABLED=1 go build "$@" 12 | mv $target $target-$arch 13 | targetlist="$targetlist $target-$arch" 14 | done 15 | 16 | echo lipo -create -output $target $targetlist 17 | lipo -create -output $target $targetlist 18 | 19 | rm $targetlist 20 | else 21 | echo GOOS=darwin GOARCH= CGO_ENABLED=1 go build $@ 22 | GOOS=darwin GOARCH= CGO_ENABLED=1 go build "$@" 23 | fi 24 | -------------------------------------------------------------------------------- /build/hubfs.wxs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 21 | 22 | 27 | 32 | 33 | 34 | 35 | 36 | 37 | $(var.LauncherRegistryKey) 38 | Software\$(var.MyProductName) 39 | 40 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 79 | 80 | 81 | 82 | 85 | 87 | 92 | 96 | 100 | 104 | 108 | 109 | 110 | 111 | 112 | 113 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | NOT Installed 140 | NOT Installed 146 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /build/make: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | make -C $(dirname "$0") "$@" 4 | -------------------------------------------------------------------------------- /build/make.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | setlocal 4 | 5 | set RegKey="HKLM\SOFTWARE\Microsoft\Windows Kits\Installed Roots" 6 | set RegVal="KitsRoot10" 7 | reg query %RegKey% /v %RegVal% >nul 2>&1 || (echo Cannot find Windows Kit >&2 & exit /b 1) 8 | for /f "tokens=2,*" %%i in ('reg query %RegKey% /v %RegVal% ^| findstr %RegVal%') do ( 9 | set KitsRoot=%%j 10 | ) 11 | for /f "tokens=*" %%i in ('reg query %RegKey% /f * /k ^| findstr "\10."') do ( 12 | set KitsInst=%%~nxi 13 | ) 14 | 15 | set PATH=%KitsRoot%bin\%KitsInst%\x64;%WIX%\bin;%PATH% 16 | rem set CPATH=C:\Program Files (x86)\WinFsp\inc\fuse 17 | set CGO_ENABLED=0 18 | 19 | for /f %%d in ('powershell -NoProfile -NonInteractive -ExecutionPolicy Unrestricted "$d=[System.DateTime]::Now; $d.ToString('yy')+$d.DayOfYear.ToString('000')"') do ( 20 | set MyBuildNumber=%%d 21 | ) 22 | 23 | mingw32-make -C %~dp0 MyBuildNumber=%MyBuildNumber% %* 24 | -------------------------------------------------------------------------------- /doc/cap1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winfsp/hubfs/c55f351edacee53d6443519375247f21e91dc9a4/doc/cap1.gif -------------------------------------------------------------------------------- /doc/mapnet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winfsp/hubfs/c55f351edacee53d6443519375247f21e91dc9a4/doc/mapnet.png -------------------------------------------------------------------------------- /src/_tools/fetch.go: -------------------------------------------------------------------------------- 1 | /* 2 | * fetch.go 3 | * 4 | * Copyright 2021-2022 Bill Zissimopoulos 5 | */ 6 | /* 7 | * This file is part of Hubfs. 8 | * 9 | * You can redistribute it and/or modify it under the terms of the GNU 10 | * Affero General Public License version 3 as published by the Free 11 | * Software Foundation. 12 | */ 13 | 14 | package main 15 | 16 | import ( 17 | "fmt" 18 | "os" 19 | "path/filepath" 20 | "strings" 21 | 22 | "github.com/winfsp/hubfs/git" 23 | ) 24 | 25 | func warn(format string, a ...interface{}) { 26 | format = "%s: " + format + "\n" 27 | a = append([]interface{}{strings.TrimSuffix(filepath.Base(os.Args[0]), ".exe")}, a...) 28 | fmt.Fprintf(os.Stderr, format, a...) 29 | } 30 | 31 | func fail(format string, a ...interface{}) { 32 | warn(format, a...) 33 | os.Exit(1) 34 | } 35 | 36 | func usage() { 37 | fmt.Println("usage: go run fetch.go repository [want...]") 38 | os.Exit(2) 39 | } 40 | 41 | func main() { 42 | if 2 > len(os.Args) { 43 | usage() 44 | } 45 | remote := os.Args[1] 46 | wants := []string{} 47 | if 2 < len(os.Args) { 48 | wants = os.Args[2:] 49 | } 50 | 51 | repository, err := git.OpenRepository(remote, "", "") 52 | if nil != err { 53 | fail("repository error: %v", err) 54 | } 55 | defer repository.Close() 56 | 57 | if 0 == len(wants) { 58 | if m, err := repository.GetRefs(); nil == err { 59 | for n, h := range m { 60 | fmt.Println(h, n) 61 | } 62 | } 63 | } else { 64 | err := repository.FetchObjects(wants, func(hash string, ot git.ObjectType, content []byte) error { 65 | switch ot { 66 | case git.CommitObject: 67 | if c, err := git.DecodeCommit(content); nil == err { 68 | fmt.Printf("commit %s\n", hash) 69 | fmt.Printf("Author : %s <%s> at %s\n", 70 | c.Author.Name, c.Author.Email, c.Author.Time) 71 | fmt.Printf("Committer: %s <%s> at %s\n", 72 | c.Committer.Name, c.Committer.Email, c.Committer.Time) 73 | fmt.Printf("TreeHash : %s\n", 74 | c.TreeHash) 75 | fmt.Println() 76 | } 77 | case git.TreeObject: 78 | if t, err := git.DecodeTree(content); nil == err { 79 | fmt.Printf("tree %s\n", hash) 80 | for _, e := range t { 81 | fmt.Printf("%06o %s %s\n", e.Mode, e.Hash, e.Name) 82 | } 83 | fmt.Println() 84 | } 85 | case git.BlobObject, git.TagObject: 86 | fmt.Printf("blob/tag %s\n", hash) 87 | if 240 < len(content) { 88 | fmt.Println(string(content[:240])) 89 | } else { 90 | fmt.Println(string(content)) 91 | } 92 | fmt.Println() 93 | } 94 | return nil 95 | }) 96 | if nil != err { 97 | fail("repository error: %v", err) 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/_tools/pmdump.go: -------------------------------------------------------------------------------- 1 | /* 2 | * pmdump.go 3 | * 4 | * Copyright 2021-2022 Bill Zissimopoulos 5 | */ 6 | /* 7 | * This file is part of Hubfs. 8 | * 9 | * You can redistribute it and/or modify it under the terms of the GNU 10 | * Affero General Public License version 3 as published by the Free 11 | * Software Foundation. 12 | */ 13 | 14 | package main 15 | 16 | import ( 17 | "fmt" 18 | "io" 19 | "io/fs" 20 | "os" 21 | pathutil "path" 22 | "path/filepath" 23 | "runtime" 24 | "strings" 25 | 26 | "github.com/winfsp/cgofuse/fuse" 27 | "github.com/winfsp/hubfs/fs/unionfs" 28 | ) 29 | 30 | type onefs struct { 31 | fuse.FileSystemBase 32 | file *os.File 33 | } 34 | 35 | func (fs *onefs) Open(path string, flags int) (int, uint64) { 36 | return 0, 0 37 | } 38 | 39 | func (fs *onefs) Read(path string, buff []byte, ofst int64, fh uint64) (n int) { 40 | n, err := fs.file.ReadAt(buff, ofst) 41 | if nil != err && io.EOF != err { 42 | n = -fuse.EIO 43 | } 44 | return 45 | } 46 | 47 | func main() { 48 | if 2 > len(os.Args) { 49 | fmt.Println("usage: go run pmdump.go pathmap [dir ...]") 50 | os.Exit(2) 51 | } 52 | 53 | file, err := os.Open(os.Args[1]) 54 | if nil != err { 55 | fmt.Fprintf(os.Stderr, "cannot open pathmap: %s\n", err) 56 | os.Exit(1) 57 | } 58 | defer file.Close() 59 | 60 | caseins := false 61 | if "windows" == runtime.GOOS || "darwin" == runtime.GOOS { 62 | caseins = true 63 | } 64 | 65 | _, pm := unionfs.OpenPathmap(&onefs{file: file}, "/.unionfs", caseins) 66 | 67 | for i, arg := range os.Args[1:] { 68 | if 0 == i { 69 | arg = filepath.Dir(arg) 70 | } 71 | filepath.Walk(arg, func(path string, info fs.FileInfo, err error) error { 72 | path, err = filepath.Rel(arg, path) 73 | if nil != err { 74 | /* do not report error */ 75 | return nil 76 | } 77 | 78 | if "windows" == runtime.GOOS { 79 | path = strings.ReplaceAll(path, `\`, `/`) 80 | } 81 | path = pathutil.Join("/", path) 82 | 83 | pm.AddDumpPath(path) 84 | 85 | return nil 86 | }) 87 | } 88 | 89 | pm.Dump(os.Stdout) 90 | } 91 | -------------------------------------------------------------------------------- /src/_tools/ptfs.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ptfs.go 3 | * 4 | * Copyright 2017-2022 Bill Zissimopoulos 5 | */ 6 | /* 7 | * This file is part of Hubfs. 8 | * 9 | * You can redistribute it and/or modify it under the terms of the GNU 10 | * Affero General Public License version 3 as published by the Free 11 | * Software Foundation. 12 | */ 13 | 14 | package main 15 | 16 | import ( 17 | "os" 18 | "path/filepath" 19 | "runtime" 20 | 21 | "github.com/winfsp/cgofuse/fuse" 22 | "github.com/winfsp/hubfs/fs/port" 23 | "github.com/winfsp/hubfs/fs/ptfs" 24 | ) 25 | 26 | func main() { 27 | port.Umask(0) 28 | 29 | args := os.Args 30 | root := "." 31 | if 3 <= len(args) && '-' != args[len(args)-2][0] && '-' != args[len(args)-1][0] { 32 | root = args[len(args)-2] 33 | args = append(args[:len(args)-2], args[len(args)-1]) 34 | } 35 | root, err := filepath.Abs(root) 36 | if nil != err { 37 | root = "." 38 | } 39 | 40 | caseins := false 41 | if "windows" == runtime.GOOS || "darwin" == runtime.GOOS { 42 | caseins = true 43 | } 44 | 45 | ptfs := ptfs.New(root) 46 | host := fuse.NewFileSystemHost(ptfs) 47 | host.SetCapReaddirPlus(true) 48 | host.SetCapCaseInsensitive(caseins) 49 | host.Mount("", args[1:]) 50 | } 51 | -------------------------------------------------------------------------------- /src/_tools/unionfs.go: -------------------------------------------------------------------------------- 1 | /* 2 | * unionfs.go 3 | * 4 | * Copyright 2021-2022 Bill Zissimopoulos 5 | */ 6 | /* 7 | * This file is part of Hubfs. 8 | * 9 | * You can redistribute it and/or modify it under the terms of the GNU 10 | * Affero General Public License version 3 as published by the Free 11 | * Software Foundation. 12 | */ 13 | 14 | package main 15 | 16 | import ( 17 | "os" 18 | "path/filepath" 19 | "runtime" 20 | "strings" 21 | 22 | "github.com/winfsp/cgofuse/fuse" 23 | "github.com/winfsp/hubfs/fs/port" 24 | "github.com/winfsp/hubfs/fs/ptfs" 25 | "github.com/winfsp/hubfs/fs/unionfs" 26 | ) 27 | 28 | func main() { 29 | port.Umask(0) 30 | 31 | args := os.Args 32 | bpos := 1 33 | epos := len(args) 34 | for i := 1; epos > i; i++ { 35 | if "-o" == args[i] { 36 | bpos = i + 2 37 | } else if strings.HasPrefix(args[i], "-") { 38 | bpos = i + 1 39 | } 40 | } 41 | if epos <= bpos { 42 | bpos = epos 43 | } 44 | if epos > bpos { 45 | epos-- 46 | } 47 | 48 | root := append([]string{}, args[bpos:epos]...) 49 | args = append(args[:bpos], args[epos:]...) 50 | if len(root) == 0 { 51 | root = []string{"."} 52 | } 53 | 54 | fslist := make([]fuse.FileSystemInterface, 0, len(root)) 55 | for _, r := range root { 56 | r, err := filepath.Abs(r) 57 | if nil != err { 58 | panic(err) 59 | } 60 | fslist = append(fslist, ptfs.New(r)) 61 | } 62 | 63 | caseins := false 64 | if "windows" == runtime.GOOS || "darwin" == runtime.GOOS { 65 | caseins = true 66 | } 67 | 68 | unfs := unionfs.New(unionfs.Config{Fslist: fslist, Caseins: caseins}) 69 | host := fuse.NewFileSystemHost(unfs) 70 | host.SetCapReaddirPlus(true) 71 | host.SetCapCaseInsensitive(caseins) 72 | host.Mount("", args[1:]) 73 | } 74 | -------------------------------------------------------------------------------- /src/fs/hubfs/hubfs.go: -------------------------------------------------------------------------------- 1 | /* 2 | * hubfs.go 3 | * 4 | * Copyright 2021-2022 Bill Zissimopoulos 5 | */ 6 | /* 7 | * This file is part of Hubfs. 8 | * 9 | * You can redistribute it and/or modify it under the terms of the GNU 10 | * Affero General Public License version 3 as published by the Free 11 | * Software Foundation. 12 | */ 13 | 14 | package hubfs 15 | 16 | import ( 17 | "io" 18 | pathutil "path" 19 | "path/filepath" 20 | "runtime" 21 | "strings" 22 | "sync" 23 | "time" 24 | 25 | libtrace "github.com/billziss-gh/golib/trace" 26 | "github.com/winfsp/cgofuse/fuse" 27 | "github.com/winfsp/hubfs/fs/port" 28 | "github.com/winfsp/hubfs/prov" 29 | ) 30 | 31 | type hubfs struct { 32 | fuse.FileSystemBase 33 | client prov.Client 34 | prefix string 35 | lock sync.RWMutex 36 | fh uint64 37 | openmap map[uint64]*obstack 38 | } 39 | 40 | type obstack struct { 41 | owner prov.Owner 42 | repository prov.Repository 43 | ref prov.Ref 44 | entry prov.TreeEntry 45 | reader io.ReaderAt 46 | } 47 | 48 | type Config struct { 49 | Client prov.Client 50 | Prefix string 51 | Caseins bool 52 | Overlay bool 53 | } 54 | 55 | func new(c Config) fuse.FileSystemInterface { 56 | return &hubfs{ 57 | client: c.Client, 58 | prefix: c.Prefix, 59 | openmap: make(map[uint64]*obstack), 60 | } 61 | } 62 | 63 | func (fs *hubfs) openex(path string, norm bool) (errc int, res *obstack, lst []string) { 64 | if strings.HasSuffix(path, "/.") { 65 | errc = -fuse.ENOENT 66 | return 67 | } 68 | 69 | lst = split(pathutil.Join(fs.prefix, path)) 70 | obs := &obstack{} 71 | var err error 72 | for i, c := range lst { 73 | switch i { 74 | case 0: 75 | // We disallow some names to speed up operations: 76 | // 77 | // - All names containing dots: e.g. ".git", ".DS_Store", "autorun.inf" 78 | // - The special git name HEAD 79 | if -1 != strings.IndexFunc(c, func(r rune) bool { return '.' == r }) || "HEAD" == c { 80 | obs.owner, err = nil, prov.ErrNotFound 81 | } else { 82 | obs.owner, err = fs.client.OpenOwner(c) 83 | if norm && nil == err { 84 | lst[i] = obs.owner.Name() 85 | } 86 | } 87 | case 1: 88 | obs.repository, err = fs.client.OpenRepository(obs.owner, c) 89 | if norm && nil == err { 90 | lst[i] = obs.repository.Name() 91 | } 92 | case 2: 93 | obs.ref, err = obs.repository.GetRef(c) 94 | if prov.ErrNotFound == err { 95 | obs.ref, err = obs.repository.GetTempRef(c) 96 | } 97 | if norm && nil == err { 98 | lst[i] = obs.ref.Name() 99 | } 100 | default: 101 | obs.entry, err = obs.repository.GetTreeEntry(obs.ref, obs.entry, c) 102 | if norm && nil == err { 103 | lst[i] = obs.entry.Name() 104 | } 105 | } 106 | if nil != err { 107 | fs.release(obs) 108 | errc = fuseErrc(err) 109 | return 110 | } 111 | } 112 | res = obs 113 | return 114 | } 115 | 116 | func (fs *hubfs) open(path string) (errc int, res *obstack) { 117 | errc, res, _ = fs.openex(path, false) 118 | return 119 | } 120 | 121 | func (fs *hubfs) release(obs *obstack) { 122 | if nil != obs.repository { 123 | fs.client.CloseRepository(obs.repository) 124 | } 125 | if nil != obs.owner { 126 | fs.client.CloseOwner(obs.owner) 127 | } 128 | } 129 | 130 | func (fs *hubfs) getattr(obs *obstack, entry prov.TreeEntry, path string, stat *fuse.Stat_t) ( 131 | target string) { 132 | 133 | if nil != entry { 134 | mode := entry.Mode() 135 | fuseStat(stat, mode, entry.Size(), obs.ref.TreeTime()) 136 | switch mode & fuse.S_IFMT { 137 | case fuse.S_IFLNK: 138 | target = entry.Target() 139 | stat.Size = int64(len(target)) 140 | case 0160000 /* submodule */ : 141 | path = pathutil.Join(fs.prefix, path) 142 | target = entry.Target() 143 | remain := repoPath(path) 144 | module, err := obs.repository.GetModule(obs.ref, remain, true) 145 | if "" != module { 146 | if t, e := filepath.Rel(pathutil.Dir(path), module+"/"+entry.Target()); nil == e { 147 | if "windows" == runtime.GOOS { 148 | t = strings.ReplaceAll(t, `\`, `/`) 149 | } 150 | target = t 151 | } else { 152 | target = strings.TrimPrefix(module, fs.prefix) + "/" + entry.Target() 153 | } 154 | } else { 155 | tracef("repo=%#v Getmodule(ref=%#v, %#v) = %v", 156 | obs.repository.Name(), obs.ref.Name(), remain, err) 157 | } 158 | stat.Size = int64(len(target)) 159 | } 160 | } else { 161 | fuseStat(stat, fuse.S_IFDIR, 0, time.Now()) 162 | } 163 | 164 | return 165 | } 166 | 167 | func (fs *hubfs) Getpath(path string, fh uint64) (errc int, normpath string) { 168 | defer trace(path, fh)(&errc, &normpath) 169 | 170 | errc0, obs, pathlst := fs.openex(path, true) 171 | if 0 == errc0 { 172 | fs.release(obs) 173 | } 174 | 175 | normpath = "/" + pathutil.Join(pathlst...) 176 | normpath = strings.TrimPrefix(normpath, fs.prefix) 177 | if "" == normpath { 178 | normpath = "/" 179 | } 180 | 181 | return 182 | } 183 | 184 | func (fs *hubfs) Getattr(path string, stat *fuse.Stat_t, fh uint64) (errc int) { 185 | defer trace(path, fh)(&errc, stat) 186 | 187 | errc, obs := fs.open(path) 188 | if 0 != errc { 189 | return 190 | } 191 | 192 | fs.getattr(obs, obs.entry, path, stat) 193 | 194 | fs.release(obs) 195 | 196 | return 197 | } 198 | 199 | func (fs *hubfs) Readlink(path string) (errc int, target string) { 200 | defer trace(path)(&errc, &target) 201 | 202 | errc, obs := fs.open(path) 203 | if 0 != errc { 204 | return 205 | } 206 | 207 | stat := fuse.Stat_t{} 208 | target = fs.getattr(obs, obs.entry, path, &stat) 209 | if "" == target { 210 | errc = -fuse.EINVAL 211 | } 212 | 213 | fs.release(obs) 214 | 215 | return 216 | } 217 | 218 | func (fs *hubfs) Opendir(path string) (errc int, fh uint64) { 219 | defer trace(path)(&errc, &fh) 220 | 221 | errc, obs := fs.open(path) 222 | if 0 != errc { 223 | return 224 | } 225 | 226 | fs.lock.Lock() 227 | fh = fs.fh 228 | fs.openmap[fh] = obs 229 | fs.fh++ 230 | fs.lock.Unlock() 231 | 232 | return 233 | } 234 | 235 | func (fs *hubfs) Readdir(path string, 236 | fill func(name string, stat *fuse.Stat_t, ofst int64) bool, 237 | ofst int64, 238 | fh uint64) (errc int) { 239 | defer trace(path, ofst, fh)(&errc) 240 | 241 | fs.lock.RLock() 242 | obs, ok := fs.openmap[fh] 243 | fs.lock.RUnlock() 244 | if !ok { 245 | errc = -fuse.ENOENT 246 | return 247 | } 248 | 249 | stat := fuse.Stat_t{} 250 | if nil != obs.entry { 251 | fuseStat(&stat, fuse.S_IFDIR, 0, obs.ref.TreeTime()) 252 | } else { 253 | fuseStat(&stat, fuse.S_IFDIR, 0, time.Now()) 254 | } 255 | fill(".", &stat, 0) 256 | fill("..", &stat, 0) 257 | 258 | if nil != obs.ref { 259 | if lst, err := obs.repository.GetTree(obs.ref, obs.entry); nil == err { 260 | for _, elm := range lst { 261 | n := elm.Name() 262 | fs.getattr(obs, elm, pathutil.Join(path, n), &stat) 263 | if !fill(n, &stat, 0) { 264 | break 265 | } 266 | } 267 | } 268 | } else if nil != obs.repository { 269 | if lst, err := obs.repository.GetRefs(); nil == err { 270 | for _, elm := range lst { 271 | if !fill(elm.Name(), &stat, 0) { 272 | break 273 | } 274 | } 275 | } 276 | } else if nil != obs.owner { 277 | if lst, err := fs.client.GetRepositories(obs.owner); nil == err { 278 | for _, elm := range lst { 279 | if !fill(elm.Name(), &stat, 0) { 280 | break 281 | } 282 | } 283 | } 284 | } else { 285 | if lst, err := fs.client.GetOwners(); nil == err { 286 | for _, elm := range lst { 287 | if !fill(elm.Name(), &stat, 0) { 288 | break 289 | } 290 | } 291 | } 292 | } 293 | 294 | return 295 | } 296 | 297 | func (fs *hubfs) Releasedir(path string, fh uint64) (errc int) { 298 | defer trace(path, fh)(&errc) 299 | 300 | fs.lock.Lock() 301 | obs, ok := fs.openmap[fh] 302 | if ok { 303 | delete(fs.openmap, fh) 304 | } 305 | fs.lock.Unlock() 306 | if !ok { 307 | errc = -fuse.ENOENT 308 | return 309 | } 310 | 311 | fs.release(obs) 312 | 313 | return 314 | } 315 | 316 | func (fs *hubfs) Open(path string, flags int) (errc int, fh uint64) { 317 | defer trace(path, flags)(&errc, &fh) 318 | 319 | errc, obs := fs.open(path) 320 | if 0 != errc { 321 | return 322 | } 323 | 324 | fs.lock.Lock() 325 | fh = fs.fh 326 | fs.openmap[fh] = obs 327 | fs.fh++ 328 | fs.lock.Unlock() 329 | 330 | return 331 | } 332 | 333 | func (fs *hubfs) Read(path string, buff []byte, ofst int64, fh uint64) (n int) { 334 | defer trace(path, ofst, fh)(&n) 335 | 336 | var reader io.ReaderAt 337 | 338 | fs.lock.RLock() 339 | obs, ok := fs.openmap[fh] 340 | if ok { 341 | reader = obs.reader 342 | } 343 | fs.lock.RUnlock() 344 | if !ok { 345 | n = -fuse.ENOENT 346 | return 347 | } 348 | 349 | if nil == reader { 350 | reader, _ = obs.repository.GetBlobReader(obs.entry) 351 | if nil == reader { 352 | n = -fuse.EIO 353 | return 354 | } 355 | 356 | var closer io.Closer 357 | fs.lock.Lock() 358 | if nil == obs.reader { 359 | obs.reader = reader 360 | } else { 361 | closer = reader.(io.Closer) 362 | reader = obs.reader 363 | } 364 | fs.lock.Unlock() 365 | if nil != closer { 366 | closer.Close() 367 | } 368 | } 369 | 370 | n, err := reader.ReadAt(buff, ofst) 371 | if nil != err && io.EOF != err { 372 | n = fuseErrc(err) 373 | return 374 | } 375 | 376 | return 377 | } 378 | 379 | func (fs *hubfs) Release(path string, fh uint64) (errc int) { 380 | defer trace(path, fh)(&errc) 381 | 382 | fs.lock.Lock() 383 | obs, ok := fs.openmap[fh] 384 | if ok { 385 | delete(fs.openmap, fh) 386 | } 387 | fs.lock.Unlock() 388 | if !ok { 389 | errc = -fuse.ENOENT 390 | return 391 | } 392 | 393 | if closer, ok := obs.reader.(io.Closer); ok { 394 | closer.Close() 395 | } 396 | 397 | fs.release(obs) 398 | 399 | return 400 | } 401 | 402 | func (self *hubfs) Statfs(path string, stat *fuse.Statfs_t) (errc int) { 403 | return port.Statfs(self.client.GetDirectory(), stat) 404 | } 405 | 406 | func fuseErrc(err error) (errc int) { 407 | errc = -fuse.EIO 408 | if prov.ErrNotFound == err { 409 | errc = -fuse.ENOENT 410 | } 411 | return 412 | } 413 | 414 | func fuseStat(stat *fuse.Stat_t, mode uint32, size int64, time time.Time) { 415 | switch mode & fuse.S_IFMT { 416 | case fuse.S_IFDIR: 417 | mode = fuse.S_IFDIR | 0755 418 | case fuse.S_IFLNK, 0160000 /* submodule */ : 419 | mode = fuse.S_IFLNK | 0777 420 | default: 421 | mode = fuse.S_IFREG | 0644 | (mode & 0111) 422 | } 423 | ts := fuse.NewTimespec(time) 424 | *stat = fuse.Stat_t{ 425 | Mode: mode, 426 | Nlink: 1, 427 | Size: size, 428 | Atim: ts, 429 | Mtim: ts, 430 | Ctim: ts, 431 | Birthtim: ts, 432 | } 433 | } 434 | 435 | func split(path string) []string { 436 | comp := strings.Split(path, "/")[1:] 437 | if 1 == len(comp) && "" == comp[0] { 438 | return []string{} 439 | } 440 | return comp 441 | } 442 | 443 | func repoPath(path string) string { 444 | slashes := 0 445 | for i := 0; len(path) > i; i++ { 446 | if '/' == path[i] { 447 | slashes++ 448 | if 4 == slashes { 449 | return path[i+1:] 450 | } 451 | } 452 | } 453 | return "" 454 | } 455 | 456 | func trace(vals ...interface{}) func(vals ...interface{}) { 457 | return libtrace.Trace(1, "", vals...) 458 | } 459 | 460 | func tracef(form string, vals ...interface{}) { 461 | libtrace.Tracef(1, form, vals...) 462 | } 463 | -------------------------------------------------------------------------------- /src/fs/hubfs/hubfs_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * hubfs_test.go 3 | * 4 | * Copyright 2021-2022 Bill Zissimopoulos 5 | */ 6 | /* 7 | * This file is part of Hubfs. 8 | * 9 | * You can redistribute it and/or modify it under the terms of the GNU 10 | * Affero General Public License version 3 as published by the Free 11 | * Software Foundation. 12 | */ 13 | 14 | package hubfs 15 | 16 | import ( 17 | "reflect" 18 | "testing" 19 | "unsafe" 20 | ) 21 | 22 | // See https://stackoverflow.com/q/42664837/568557 23 | func testGetUnexportedField(field reflect.Value) reflect.Value { 24 | return reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem() 25 | } 26 | 27 | func TestNewOverlay(t *testing.T) { 28 | P := []string{"", "/1", "/1/2", "/1/2/3"} 29 | Q := []string{"/", "/a", "/a/b", "/a/b/c", "/a/b/c/d"} 30 | E := []struct{ prefix, remain string }{ 31 | {"", "/"}, 32 | {"", "/a"}, 33 | {"", "/a/b"}, 34 | {"/a/b/c", "/"}, 35 | {"/a/b/c", "/d"}, 36 | {"", "/"}, 37 | {"", "/a"}, 38 | {"/a/b", "/"}, 39 | {"/a/b", "/c"}, 40 | {"/a/b", "/c/d"}, 41 | {"", "/"}, 42 | {"/a", "/"}, 43 | {"/a", "/b"}, 44 | {"/a", "/b/c"}, 45 | {"/a", "/b/c/d"}, 46 | {"/", "/"}, 47 | {"/", "/a"}, 48 | {"/", "/a/b"}, 49 | {"/", "/a/b/c"}, 50 | {"/", "/a/b/c/d"}, 51 | } 52 | i := 0 53 | for _, p := range P { 54 | fs := newOverlay(Config{Prefix: p}) 55 | split := testGetUnexportedField(reflect.ValueOf(fs).Elem().FieldByName("split")) 56 | for _, q := range Q { 57 | a := make([]reflect.Value, 1) 58 | a[0] = reflect.ValueOf(q) 59 | r := split.Call(a) 60 | prefix, remain := r[0].String(), r[1].String() 61 | if prefix != E[i].prefix || remain != E[i].remain { 62 | t.Error() 63 | } 64 | i++ 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/fs/hubfs/overlay.go: -------------------------------------------------------------------------------- 1 | /* 2 | * overlay.go 3 | * 4 | * Copyright 2021-2022 Bill Zissimopoulos 5 | */ 6 | /* 7 | * This file is part of Hubfs. 8 | * 9 | * You can redistribute it and/or modify it under the terms of the GNU 10 | * Affero General Public License version 3 as published by the Free 11 | * Software Foundation. 12 | */ 13 | 14 | package hubfs 15 | 16 | import ( 17 | "os" 18 | pathutil "path" 19 | "path/filepath" 20 | "strings" 21 | "time" 22 | 23 | "github.com/winfsp/cgofuse/fuse" 24 | "github.com/winfsp/hubfs/fs/overlayfs" 25 | "github.com/winfsp/hubfs/fs/port" 26 | "github.com/winfsp/hubfs/fs/ptfs" 27 | "github.com/winfsp/hubfs/fs/unionfs" 28 | ) 29 | 30 | func New(c Config) fuse.FileSystemInterface { 31 | /* if have Prefix, clean it up and make sure it does not have more than 3 components */ 32 | c.Prefix = pathutil.Clean(c.Prefix) 33 | switch c.Prefix { 34 | case "/", ".": 35 | c.Prefix = "" 36 | } 37 | slashes := 0 38 | for i := 0; len(c.Prefix) > i; i++ { 39 | if '/' == c.Prefix[i] { 40 | slashes++ 41 | if 4 == slashes { 42 | c.Prefix = c.Prefix[:i] 43 | break 44 | } 45 | } 46 | } 47 | 48 | if c.Overlay { 49 | return newOverlay(c) 50 | } else { 51 | return new(c) 52 | } 53 | } 54 | 55 | func newOverlay(c Config) fuse.FileSystemInterface { 56 | scope := c.Prefix 57 | scopeSlashes := strings.Count(c.Prefix, "/") 58 | caseins := c.Caseins 59 | 60 | topfs := new(Config{ 61 | Client: c.Client, 62 | Prefix: c.Prefix, 63 | Caseins: c.Caseins, 64 | }).(*hubfs) 65 | 66 | split := func(path string) (string, string) { 67 | slashes := scopeSlashes 68 | for i := 0; len(path) > i; i++ { 69 | if '/' == path[i] { 70 | slashes++ 71 | if 4 == slashes { 72 | if 0 == i { 73 | return "/", path 74 | } else { 75 | return path[:i], path[i:] 76 | } 77 | } 78 | } 79 | } 80 | if 3 == slashes && "/" != path { 81 | return path, "/" 82 | } 83 | return "", path 84 | } 85 | 86 | newfs := func(prefix string) fuse.FileSystemInterface { 87 | defer func() { 88 | if r := recover(); nil != r { 89 | tracef("prefix=%q !PANIC:%v", prefix, r) 90 | } 91 | }() 92 | 93 | errc, obs := topfs.open(prefix) 94 | if 0 != errc { 95 | return nil 96 | } 97 | 98 | root := filepath.Join(obs.repository.GetDirectory(), "files") 99 | err := os.MkdirAll(root, 0700) 100 | if nil != err { 101 | topfs.release(obs) 102 | return nil 103 | } 104 | 105 | root = filepath.Join(root, obs.ref.Name()) 106 | err = os.MkdirAll(root, 0755) 107 | if nil != err { 108 | topfs.release(obs) 109 | return nil 110 | } 111 | 112 | errc, root = port.Realpath(root) 113 | if 0 != errc { 114 | topfs.release(obs) 115 | return nil 116 | } 117 | 118 | upfs := ptfs.New(root) 119 | lofs := new(Config{ 120 | Client: topfs.client, 121 | Prefix: pathutil.Join(scope, prefix), 122 | Caseins: caseins, 123 | }) 124 | unfs := unionfs.New(unionfs.Config{ 125 | Fslist: []fuse.FileSystemInterface{upfs, lofs}, 126 | Caseins: caseins, 127 | }) 128 | 129 | return newShardfs(topfs, prefix, obs, unfs) 130 | } 131 | 132 | return overlayfs.New(overlayfs.Config{ 133 | Topfs: topfs, 134 | Split: split, 135 | Newfs: newfs, 136 | Caseins: caseins, 137 | TimeToLive: 1 * time.Second, 138 | }) 139 | } 140 | -------------------------------------------------------------------------------- /src/fs/hubfs/shardfs.go: -------------------------------------------------------------------------------- 1 | /* 2 | * shardfs.go 3 | * 4 | * Copyright 2021-2022 Bill Zissimopoulos 5 | */ 6 | /* 7 | * This file is part of Hubfs. 8 | * 9 | * You can redistribute it and/or modify it under the terms of the GNU 10 | * Affero General Public License version 3 as published by the Free 11 | * Software Foundation. 12 | */ 13 | 14 | package hubfs 15 | 16 | import ( 17 | "sync" 18 | 19 | "github.com/winfsp/cgofuse/fuse" 20 | ) 21 | 22 | type shardfs struct { 23 | fuse.FileSystemInterface 24 | fuse.FileSystemGetpath 25 | topfs *hubfs 26 | prefix string 27 | obs *obstack 28 | keeppath string 29 | once sync.Once 30 | } 31 | 32 | func newShardfs(topfs *hubfs, prefix string, obs *obstack, fs fuse.FileSystemInterface) fuse.FileSystemInterface { 33 | return &shardfs{ 34 | FileSystemInterface: fs, 35 | FileSystemGetpath: fs.(fuse.FileSystemGetpath), 36 | topfs: topfs, 37 | prefix: prefix, 38 | obs: obs, 39 | keeppath: "/.keep", 40 | } 41 | } 42 | 43 | func (fs *shardfs) initonce() { 44 | fs.once.Do(func() { 45 | errc, fh := fs.FileSystemInterface.Create(fs.keeppath, fuse.O_CREAT|fuse.O_RDWR, 0644) 46 | if -fuse.ENOSYS == errc { 47 | errc = fs.FileSystemInterface.Mknod(fs.keeppath, 0644, 0) 48 | if 0 == errc { 49 | errc, fh = fs.FileSystemInterface.Open(fs.keeppath, fuse.O_RDWR) 50 | } 51 | } 52 | if 0 == errc { 53 | fs.FileSystemInterface.Release(fs.keeppath, fh) 54 | } 55 | }) 56 | } 57 | 58 | func (fs *shardfs) Destroy() { 59 | fs.FileSystemInterface.Destroy() 60 | fs.topfs.release(fs.obs) 61 | } 62 | 63 | func (fs *shardfs) Mknod(path string, mode uint32, dev uint64) (errc int) { 64 | errc = fs.FileSystemInterface.Mknod(path, mode, dev) 65 | if 0 == errc { 66 | fs.initonce() 67 | } 68 | return 69 | } 70 | 71 | func (fs *shardfs) Mkdir(path string, mode uint32) (errc int) { 72 | errc = fs.FileSystemInterface.Mkdir(path, mode) 73 | if 0 == errc { 74 | fs.initonce() 75 | } 76 | return 77 | } 78 | 79 | func (fs *shardfs) Unlink(path string) (errc int) { 80 | errc = fs.FileSystemInterface.Unlink(path) 81 | if 0 == errc && fs.keeppath != path { 82 | fs.initonce() 83 | } 84 | return 85 | } 86 | 87 | func (fs *shardfs) Rmdir(path string) (errc int) { 88 | errc = fs.FileSystemInterface.Rmdir(path) 89 | if 0 == errc { 90 | fs.initonce() 91 | } 92 | return 93 | } 94 | 95 | func (fs *shardfs) Link(oldpath string, newpath string) (errc int) { 96 | errc = fs.FileSystemInterface.Link(oldpath, newpath) 97 | if 0 == errc { 98 | fs.initonce() 99 | } 100 | return 101 | } 102 | 103 | func (fs *shardfs) Symlink(target string, newpath string) (errc int) { 104 | errc = fs.FileSystemInterface.Symlink(target, newpath) 105 | if 0 == errc { 106 | fs.initonce() 107 | } 108 | return 109 | } 110 | 111 | func (fs *shardfs) Rename(oldpath string, newpath string) (errc int) { 112 | errc = fs.FileSystemInterface.Rename(oldpath, newpath) 113 | if 0 == errc { 114 | fs.initonce() 115 | } 116 | return 117 | } 118 | 119 | func (fs *shardfs) Chmod(path string, mode uint32) (errc int) { 120 | errc = fs.FileSystemInterface.Chmod(path, mode) 121 | if 0 == errc { 122 | fs.initonce() 123 | } 124 | return 125 | } 126 | 127 | func (fs *shardfs) Chown(path string, uid uint32, gid uint32) (errc int) { 128 | errc = fs.FileSystemInterface.Chown(path, uid, gid) 129 | if 0 == errc { 130 | fs.initonce() 131 | } 132 | return 133 | } 134 | 135 | func (fs *shardfs) Utimens(path string, tmsp []fuse.Timespec) (errc int) { 136 | errc = fs.FileSystemInterface.Utimens(path, tmsp) 137 | if 0 == errc { 138 | fs.initonce() 139 | } 140 | return 141 | } 142 | 143 | func (fs *shardfs) Create(path string, flags int, mode uint32) (errc int, fh uint64) { 144 | errc, fh = fs.FileSystemInterface.Create(path, flags, mode) 145 | if 0 == errc { 146 | fs.initonce() 147 | } 148 | return 149 | } 150 | 151 | func (fs *shardfs) Truncate(path string, size int64, fh uint64) (errc int) { 152 | errc = fs.FileSystemInterface.Truncate(path, size, fh) 153 | if 0 == errc { 154 | fs.initonce() 155 | } 156 | return 157 | } 158 | 159 | func (fs *shardfs) Write(path string, buff []byte, ofst int64, fh uint64) (n int) { 160 | n = fs.FileSystemInterface.Write(path, buff, ofst, fh) 161 | if 0 <= n { 162 | fs.initonce() 163 | } 164 | return 165 | } 166 | 167 | func (fs *shardfs) Setxattr(path string, name string, value []byte, flags int) (errc int) { 168 | errc = fs.FileSystemInterface.Setxattr(path, name, value, flags) 169 | if 0 == errc { 170 | fs.initonce() 171 | } 172 | return 173 | } 174 | 175 | func (fs *shardfs) Removexattr(path string, name string) (errc int) { 176 | errc = fs.FileSystemInterface.Removexattr(path, name) 177 | if 0 == errc { 178 | fs.initonce() 179 | } 180 | return 181 | } 182 | 183 | func (fs *shardfs) Chflags(path string, flags uint32) (errc int) { 184 | /* lie! */ 185 | return 0 186 | } 187 | 188 | func (fs *shardfs) Setcrtime(path string, tmsp fuse.Timespec) (errc int) { 189 | /* lie! */ 190 | return 0 191 | } 192 | 193 | func (fs *shardfs) Setchgtime(path string, tmsp fuse.Timespec) (errc int) { 194 | /* lie! */ 195 | return 0 196 | } 197 | 198 | var _ fuse.FileSystemInterface = (*shardfs)(nil) 199 | var _ fuse.FileSystemGetpath = (*shardfs)(nil) 200 | var _ fuse.FileSystemChflags = (*shardfs)(nil) 201 | var _ fuse.FileSystemSetcrtime = (*shardfs)(nil) 202 | var _ fuse.FileSystemSetchgtime = (*shardfs)(nil) 203 | -------------------------------------------------------------------------------- /src/fs/nullfs/nullfs.go: -------------------------------------------------------------------------------- 1 | /* 2 | * nullfs.go 3 | * 4 | * Copyright 2021-2022 Bill Zissimopoulos 5 | */ 6 | /* 7 | * This file is part of Hubfs. 8 | * 9 | * You can redistribute it and/or modify it under the terms of the GNU 10 | * Affero General Public License version 3 as published by the Free 11 | * Software Foundation. 12 | */ 13 | 14 | package nullfs 15 | 16 | import ( 17 | "github.com/winfsp/cgofuse/fuse" 18 | ) 19 | 20 | type filesystem struct { 21 | } 22 | 23 | func New() fuse.FileSystemInterface { 24 | return &filesystem{} 25 | } 26 | 27 | func (fs *filesystem) Init() { 28 | } 29 | 30 | func (fs *filesystem) Destroy() { 31 | } 32 | 33 | func (fs *filesystem) Statfs(path string, stat *fuse.Statfs_t) (errc int) { 34 | return -fuse.ENOENT 35 | } 36 | 37 | func (fs *filesystem) Mknod(path string, mode uint32, dev uint64) (errc int) { 38 | return -fuse.EPERM 39 | } 40 | 41 | func (fs *filesystem) Mkdir(path string, mode uint32) (errc int) { 42 | return -fuse.EPERM 43 | } 44 | 45 | func (fs *filesystem) Unlink(path string) (errc int) { 46 | return -fuse.ENOENT 47 | } 48 | 49 | func (fs *filesystem) Rmdir(path string) (errc int) { 50 | return -fuse.ENOENT 51 | } 52 | 53 | func (fs *filesystem) Link(oldpath string, newpath string) (errc int) { 54 | return -fuse.ENOENT 55 | } 56 | 57 | func (fs *filesystem) Symlink(target string, newpath string) (errc int) { 58 | return -fuse.EPERM 59 | } 60 | 61 | func (fs *filesystem) Readlink(path string) (errc int, target string) { 62 | return -fuse.ENOENT, "" 63 | } 64 | 65 | func (fs *filesystem) Rename(oldpath string, newpath string) (errc int) { 66 | return -fuse.ENOENT 67 | } 68 | 69 | func (fs *filesystem) Chmod(path string, mode uint32) (errc int) { 70 | return -fuse.ENOENT 71 | } 72 | 73 | func (fs *filesystem) Chown(path string, uid uint32, gid uint32) (errc int) { 74 | return -fuse.ENOENT 75 | } 76 | 77 | func (fs *filesystem) Utimens(path string, tmsp []fuse.Timespec) (errc int) { 78 | return -fuse.ENOENT 79 | } 80 | 81 | func (fs *filesystem) Access(path string, mask uint32) (errc int) { 82 | return -fuse.ENOENT 83 | } 84 | 85 | func (fs *filesystem) Create(path string, flags int, mode uint32) (errc int, fh uint64) { 86 | return -fuse.EPERM, ^uint64(0) 87 | } 88 | 89 | func (fs *filesystem) Open(path string, flags int) (errc int, fh uint64) { 90 | return -fuse.ENOENT, ^uint64(0) 91 | } 92 | 93 | func (fs *filesystem) Getattr(path string, stat *fuse.Stat_t, fh uint64) (errc int) { 94 | return -fuse.ENOENT 95 | } 96 | 97 | func (fs *filesystem) Truncate(path string, size int64, fh uint64) (errc int) { 98 | return -fuse.ENOENT 99 | } 100 | 101 | func (fs *filesystem) Read(path string, buff []byte, ofst int64, fh uint64) (n int) { 102 | return -fuse.ENOENT 103 | } 104 | 105 | func (fs *filesystem) Write(path string, buff []byte, ofst int64, fh uint64) (n int) { 106 | return -fuse.ENOENT 107 | } 108 | 109 | func (fs *filesystem) Flush(path string, fh uint64) (errc int) { 110 | return -fuse.ENOENT 111 | } 112 | 113 | func (fs *filesystem) Release(path string, fh uint64) (errc int) { 114 | return -fuse.ENOENT 115 | } 116 | 117 | func (fs *filesystem) Fsync(path string, datasync bool, fh uint64) (errc int) { 118 | return -fuse.ENOENT 119 | } 120 | 121 | func (fs *filesystem) Opendir(path string) (errc int, fh uint64) { 122 | return -fuse.ENOENT, ^uint64(0) 123 | } 124 | 125 | func (fs *filesystem) Readdir(path string, 126 | fill func(name string, stat *fuse.Stat_t, ofst int64) bool, 127 | ofst int64, 128 | fh uint64) (errc int) { 129 | return -fuse.ENOENT 130 | } 131 | 132 | func (fs *filesystem) Releasedir(path string, fh uint64) (errc int) { 133 | return -fuse.ENOENT 134 | } 135 | 136 | func (fs *filesystem) Fsyncdir(path string, datasync bool, fh uint64) (errc int) { 137 | return -fuse.ENOENT 138 | } 139 | 140 | func (fs *filesystem) Setxattr(path string, name string, value []byte, flags int) (errc int) { 141 | return -fuse.ENOENT 142 | } 143 | 144 | func (fs *filesystem) Getxattr(path string, name string) (errc int, value []byte) { 145 | return -fuse.ENOENT, nil 146 | } 147 | 148 | func (fs *filesystem) Removexattr(path string, name string) (errc int) { 149 | return -fuse.ENOENT 150 | } 151 | 152 | func (fs *filesystem) Listxattr(path string, fill func(name string) bool) (errc int) { 153 | return -fuse.ENOENT 154 | } 155 | 156 | func (fs *filesystem) Getpath(path string, fh uint64) (errc int, normpath string) { 157 | return -fuse.ENOENT, "" 158 | } 159 | 160 | func (fs *filesystem) Chflags(path string, flags uint32) (errc int) { 161 | return -fuse.ENOENT 162 | } 163 | 164 | func (fs *filesystem) Setcrtime(path string, tmsp fuse.Timespec) (errc int) { 165 | return -fuse.ENOENT 166 | } 167 | 168 | func (fs *filesystem) Setchgtime(path string, tmsp fuse.Timespec) (errc int) { 169 | return -fuse.ENOENT 170 | } 171 | 172 | var _ fuse.FileSystemInterface = (*filesystem)(nil) 173 | var _ fuse.FileSystemGetpath = (*filesystem)(nil) 174 | var _ fuse.FileSystemChflags = (*filesystem)(nil) 175 | var _ fuse.FileSystemSetcrtime = (*filesystem)(nil) 176 | var _ fuse.FileSystemSetchgtime = (*filesystem)(nil) 177 | -------------------------------------------------------------------------------- /src/fs/overlayfs/overlayfs.go: -------------------------------------------------------------------------------- 1 | /* 2 | * overlayfs.go 3 | * 4 | * Copyright 2021-2022 Bill Zissimopoulos 5 | */ 6 | /* 7 | * This file is part of Hubfs. 8 | * 9 | * You can redistribute it and/or modify it under the terms of the GNU 10 | * Affero General Public License version 3 as published by the Free 11 | * Software Foundation. 12 | */ 13 | 14 | package overlayfs 15 | 16 | import ( 17 | pathutil "path" 18 | "strings" 19 | "sync" 20 | "time" 21 | 22 | "github.com/winfsp/cgofuse/fuse" 23 | "github.com/winfsp/hubfs/fs/nullfs" 24 | ) 25 | 26 | type filesystem struct { 27 | topfs *shardfs 28 | split func(path string) (string, string) 29 | newfs func(prefix string) fuse.FileSystemInterface 30 | caseins bool 31 | ttl time.Duration 32 | fsmux sync.Mutex 33 | fsmap map[string]*shardfs 34 | nullfs *shardfs 35 | } 36 | 37 | type shardfs struct { 38 | fuse.FileSystemInterface 39 | prefix string 40 | normprefix string 41 | rc int 42 | timer *time.Timer 43 | } 44 | 45 | type Config struct { 46 | Topfs fuse.FileSystemInterface 47 | Split func(path string) (string, string) 48 | Newfs func(prefix string) fuse.FileSystemInterface 49 | Caseins bool 50 | TimeToLive time.Duration 51 | } 52 | 53 | func New(c Config) fuse.FileSystemInterface { 54 | return &filesystem{ 55 | topfs: &shardfs{FileSystemInterface: c.Topfs, rc: -1}, 56 | split: c.Split, 57 | newfs: c.Newfs, 58 | caseins: c.Caseins, 59 | ttl: c.TimeToLive, 60 | fsmap: make(map[string]*shardfs), 61 | nullfs: &shardfs{FileSystemInterface: nullfs.New(), rc: -1}, 62 | } 63 | } 64 | 65 | func (fs *filesystem) acquirefs(path string, delta int) (dstfs *shardfs, remain string) { 66 | prefix, remain := fs.split(path) 67 | if "" == prefix { 68 | dstfs = fs.topfs 69 | return 70 | } 71 | 72 | csprefix := prefix 73 | if fs.caseins { 74 | prefix = strings.ToUpper(prefix) 75 | } 76 | 77 | fs.fsmux.Lock() 78 | dstfs = fs.fsmap[prefix] 79 | if nil == dstfs { 80 | if newfs := fs.newfs(csprefix); nil != newfs { 81 | dstfs = &shardfs{FileSystemInterface: newfs, prefix: prefix, normprefix: prefix} 82 | fs.fsmap[prefix] = dstfs 83 | dstfs.Init() 84 | dstfs.rc += delta 85 | if intf, ok := fs.topfs.FileSystemInterface.(fuse.FileSystemGetpath); ok { 86 | if errc, normprefix := intf.Getpath(prefix, ^uint64(0)); 0 == errc { 87 | dstfs.normprefix = normprefix 88 | } 89 | } 90 | } else { 91 | dstfs = fs.nullfs 92 | } 93 | } else { 94 | dstfs.rc += delta 95 | } 96 | fs.fsmux.Unlock() 97 | return 98 | } 99 | 100 | func (fs *filesystem) releasefs(dstfs *shardfs, delta int, errc *int) { 101 | if (nil == errc || 0 != *errc) && 102 | !(0 > dstfs.rc) /* high bit of dstfs.rc is stable in presence of multiple threads */ { 103 | fs.fsmux.Lock() 104 | dstfs.rc += delta 105 | if 0 == dstfs.rc { 106 | if 0 == fs.ttl { 107 | dstfs.Destroy() 108 | delete(fs.fsmap, dstfs.prefix) 109 | } else { 110 | if nil == dstfs.timer { 111 | dstfs.timer = time.AfterFunc(fs.ttl, func() { 112 | fs._expirefs(dstfs) 113 | }) 114 | } else { 115 | dstfs.timer.Reset(fs.ttl) 116 | } 117 | } 118 | } 119 | fs.fsmux.Unlock() 120 | } 121 | } 122 | 123 | func (fs *filesystem) _expirefs(dstfs *shardfs) { 124 | fs.fsmux.Lock() 125 | if 0 == dstfs.rc && dstfs == fs.fsmap[dstfs.prefix] { 126 | dstfs.Destroy() 127 | delete(fs.fsmap, dstfs.prefix) 128 | } 129 | fs.fsmux.Unlock() 130 | } 131 | 132 | func (fs *filesystem) Init() { 133 | fs.topfs.Init() 134 | } 135 | 136 | func (fs *filesystem) Destroy() { 137 | fs.fsmux.Lock() 138 | for _, fs := range fs.fsmap { 139 | fs.Destroy() 140 | } 141 | fs.fsmap = make(map[string]*shardfs) 142 | fs.fsmux.Unlock() 143 | 144 | fs.topfs.Destroy() 145 | } 146 | 147 | func (fs *filesystem) Statfs(path string, stat *fuse.Statfs_t) (errc int) { 148 | dstfs, path := fs.acquirefs(path, +1) 149 | defer fs.releasefs(dstfs, -1, nil) 150 | return dstfs.Statfs(path, stat) 151 | } 152 | 153 | func (fs *filesystem) Mknod(path string, mode uint32, dev uint64) (errc int) { 154 | dstfs, path := fs.acquirefs(path, +1) 155 | defer fs.releasefs(dstfs, -1, nil) 156 | return dstfs.Mknod(path, mode, dev) 157 | } 158 | 159 | func (fs *filesystem) Mkdir(path string, mode uint32) (errc int) { 160 | dstfs, path := fs.acquirefs(path, +1) 161 | defer fs.releasefs(dstfs, -1, nil) 162 | return dstfs.Mkdir(path, mode) 163 | } 164 | 165 | func (fs *filesystem) Unlink(path string) (errc int) { 166 | dstfs, path := fs.acquirefs(path, +1) 167 | defer fs.releasefs(dstfs, -1, nil) 168 | return dstfs.Unlink(path) 169 | } 170 | 171 | func (fs *filesystem) Rmdir(path string) (errc int) { 172 | dstfs, path := fs.acquirefs(path, +1) 173 | defer fs.releasefs(dstfs, -1, nil) 174 | return dstfs.Rmdir(path) 175 | } 176 | 177 | func (fs *filesystem) Link(oldpath string, newpath string) (errc int) { 178 | oldprefix, _ := fs.split(oldpath) 179 | newprefix, newpath := fs.split(newpath) 180 | if fs.caseins { 181 | if strings.ToUpper(oldprefix) != strings.ToUpper(newprefix) { 182 | return -fuse.EXDEV 183 | } 184 | } else { 185 | if oldprefix != newprefix { 186 | return -fuse.EXDEV 187 | } 188 | } 189 | dstfs, oldpath := fs.acquirefs(oldpath, +1) 190 | defer fs.releasefs(dstfs, -1, nil) 191 | return dstfs.Link(oldpath, newpath) 192 | } 193 | 194 | func (fs *filesystem) Symlink(target string, newpath string) (errc int) { 195 | dstfs, newpath := fs.acquirefs(newpath, +1) 196 | defer fs.releasefs(dstfs, -1, nil) 197 | return dstfs.Symlink(target, newpath) 198 | } 199 | 200 | func (fs *filesystem) Readlink(path string) (errc int, target string) { 201 | dstfs, path := fs.acquirefs(path, +1) 202 | defer fs.releasefs(dstfs, -1, nil) 203 | return dstfs.Readlink(path) 204 | } 205 | 206 | func (fs *filesystem) Rename(oldpath string, newpath string) (errc int) { 207 | oldprefix, _ := fs.split(oldpath) 208 | newprefix, newpath := fs.split(newpath) 209 | if fs.caseins { 210 | if strings.ToUpper(oldprefix) != strings.ToUpper(newprefix) { 211 | return -fuse.EXDEV 212 | } 213 | } else { 214 | if oldprefix != newprefix { 215 | return -fuse.EXDEV 216 | } 217 | } 218 | dstfs, oldpath := fs.acquirefs(oldpath, +1) 219 | defer fs.releasefs(dstfs, -1, nil) 220 | return dstfs.Rename(oldpath, newpath) 221 | } 222 | 223 | func (fs *filesystem) Chmod(path string, mode uint32) (errc int) { 224 | dstfs, path := fs.acquirefs(path, +1) 225 | defer fs.releasefs(dstfs, -1, nil) 226 | return dstfs.Chmod(path, mode) 227 | } 228 | 229 | func (fs *filesystem) Chown(path string, uid uint32, gid uint32) (errc int) { 230 | dstfs, path := fs.acquirefs(path, +1) 231 | defer fs.releasefs(dstfs, -1, nil) 232 | return dstfs.Chown(path, uid, gid) 233 | } 234 | 235 | func (fs *filesystem) Utimens(path string, tmsp []fuse.Timespec) (errc int) { 236 | dstfs, path := fs.acquirefs(path, +1) 237 | defer fs.releasefs(dstfs, -1, nil) 238 | return dstfs.Utimens(path, tmsp) 239 | } 240 | 241 | func (fs *filesystem) Access(path string, mask uint32) (errc int) { 242 | dstfs, path := fs.acquirefs(path, +1) 243 | defer fs.releasefs(dstfs, -1, nil) 244 | return dstfs.Access(path, mask) 245 | } 246 | 247 | func (fs *filesystem) Create(path string, flags int, mode uint32) (errc int, fh uint64) { 248 | dstfs, path := fs.acquirefs(path, +1) 249 | defer fs.releasefs(dstfs, -1, &errc) 250 | return dstfs.Create(path, flags, mode) 251 | } 252 | 253 | func (fs *filesystem) Open(path string, flags int) (errc int, fh uint64) { 254 | dstfs, path := fs.acquirefs(path, +1) 255 | defer fs.releasefs(dstfs, -1, &errc) 256 | return dstfs.Open(path, flags) 257 | } 258 | 259 | func (fs *filesystem) Getattr(path string, stat *fuse.Stat_t, fh uint64) (errc int) { 260 | dstfs, path := fs.acquirefs(path, +1) 261 | defer fs.releasefs(dstfs, -1, nil) 262 | return dstfs.Getattr(path, stat, fh) 263 | } 264 | 265 | func (fs *filesystem) Truncate(path string, size int64, fh uint64) (errc int) { 266 | dstfs, path := fs.acquirefs(path, +1) 267 | defer fs.releasefs(dstfs, -1, nil) 268 | return dstfs.Truncate(path, size, fh) 269 | } 270 | 271 | func (fs *filesystem) Read(path string, buff []byte, ofst int64, fh uint64) (n int) { 272 | dstfs, path := fs.acquirefs(path, 0) 273 | return dstfs.Read(path, buff, ofst, fh) 274 | } 275 | 276 | func (fs *filesystem) Write(path string, buff []byte, ofst int64, fh uint64) (n int) { 277 | dstfs, path := fs.acquirefs(path, 0) 278 | return dstfs.Write(path, buff, ofst, fh) 279 | } 280 | 281 | func (fs *filesystem) Flush(path string, fh uint64) (errc int) { 282 | dstfs, path := fs.acquirefs(path, 0) 283 | return dstfs.Flush(path, fh) 284 | } 285 | 286 | func (fs *filesystem) Release(path string, fh uint64) (errc int) { 287 | dstfs, path := fs.acquirefs(path, 0) 288 | defer fs.releasefs(dstfs, -1, nil) 289 | return dstfs.Release(path, fh) 290 | } 291 | 292 | func (fs *filesystem) Fsync(path string, datasync bool, fh uint64) (errc int) { 293 | dstfs, path := fs.acquirefs(path, 0) 294 | return dstfs.Fsync(path, datasync, fh) 295 | } 296 | 297 | func (fs *filesystem) Opendir(path string) (errc int, fh uint64) { 298 | dstfs, path := fs.acquirefs(path, +1) 299 | defer fs.releasefs(dstfs, -1, &errc) 300 | return dstfs.Opendir(path) 301 | } 302 | 303 | func (fs *filesystem) Readdir(path string, 304 | fill func(name string, stat *fuse.Stat_t, ofst int64) bool, 305 | ofst int64, 306 | fh uint64) (errc int) { 307 | dstfs, path := fs.acquirefs(path, 0) 308 | return dstfs.Readdir(path, fill, ofst, fh) 309 | } 310 | 311 | func (fs *filesystem) Releasedir(path string, fh uint64) (errc int) { 312 | dstfs, path := fs.acquirefs(path, 0) 313 | defer fs.releasefs(dstfs, -1, nil) 314 | return dstfs.Releasedir(path, fh) 315 | } 316 | 317 | func (fs *filesystem) Fsyncdir(path string, datasync bool, fh uint64) (errc int) { 318 | dstfs, path := fs.acquirefs(path, 0) 319 | return dstfs.Fsyncdir(path, datasync, fh) 320 | } 321 | 322 | func (fs *filesystem) Setxattr(path string, name string, value []byte, flags int) (errc int) { 323 | dstfs, path := fs.acquirefs(path, +1) 324 | defer fs.releasefs(dstfs, -1, nil) 325 | return dstfs.Setxattr(path, name, value, flags) 326 | } 327 | 328 | func (fs *filesystem) Getxattr(path string, name string) (errc int, value []byte) { 329 | dstfs, path := fs.acquirefs(path, +1) 330 | defer fs.releasefs(dstfs, -1, nil) 331 | return dstfs.Getxattr(path, name) 332 | } 333 | 334 | func (fs *filesystem) Removexattr(path string, name string) (errc int) { 335 | dstfs, path := fs.acquirefs(path, +1) 336 | defer fs.releasefs(dstfs, -1, nil) 337 | return dstfs.Removexattr(path, name) 338 | } 339 | 340 | func (fs *filesystem) Listxattr(path string, fill func(name string) bool) (errc int) { 341 | dstfs, path := fs.acquirefs(path, +1) 342 | defer fs.releasefs(dstfs, -1, nil) 343 | return dstfs.Listxattr(path, fill) 344 | } 345 | 346 | func (fs *filesystem) Getpath(path string, fh uint64) (errc int, normpath string) { 347 | dstfs, path := fs.acquirefs(path, +1) 348 | defer fs.releasefs(dstfs, -1, nil) 349 | intf, ok := dstfs.FileSystemInterface.(fuse.FileSystemGetpath) 350 | if !ok { 351 | return -fuse.ENOSYS, "" 352 | } 353 | errc, normpath = intf.Getpath(path, fh) 354 | if 0 == errc { 355 | normpath = pathutil.Join(dstfs.normprefix, normpath) 356 | } 357 | return 358 | } 359 | 360 | func (fs *filesystem) Chflags(path string, flags uint32) (errc int) { 361 | dstfs, path := fs.acquirefs(path, +1) 362 | defer fs.releasefs(dstfs, -1, nil) 363 | intf, ok := dstfs.FileSystemInterface.(fuse.FileSystemChflags) 364 | if !ok { 365 | return -fuse.ENOSYS 366 | } 367 | return intf.Chflags(path, flags) 368 | } 369 | 370 | func (fs *filesystem) Setcrtime(path string, tmsp fuse.Timespec) (errc int) { 371 | dstfs, path := fs.acquirefs(path, +1) 372 | defer fs.releasefs(dstfs, -1, nil) 373 | intf, ok := dstfs.FileSystemInterface.(fuse.FileSystemSetcrtime) 374 | if !ok { 375 | return -fuse.ENOSYS 376 | } 377 | return intf.Setcrtime(path, tmsp) 378 | } 379 | 380 | func (fs *filesystem) Setchgtime(path string, tmsp fuse.Timespec) (errc int) { 381 | dstfs, path := fs.acquirefs(path, +1) 382 | defer fs.releasefs(dstfs, -1, nil) 383 | intf, ok := dstfs.FileSystemInterface.(fuse.FileSystemSetchgtime) 384 | if !ok { 385 | return -fuse.ENOSYS 386 | } 387 | return intf.Setchgtime(path, tmsp) 388 | } 389 | 390 | var _ fuse.FileSystemInterface = (*filesystem)(nil) 391 | var _ fuse.FileSystemGetpath = (*filesystem)(nil) 392 | var _ fuse.FileSystemChflags = (*filesystem)(nil) 393 | var _ fuse.FileSystemSetcrtime = (*filesystem)(nil) 394 | var _ fuse.FileSystemSetchgtime = (*filesystem)(nil) 395 | -------------------------------------------------------------------------------- /src/fs/port/port_darwin.go: -------------------------------------------------------------------------------- 1 | //go:build darwin 2 | // +build darwin 3 | 4 | /* 5 | * port_darwin.go 6 | * 7 | * Copyright 2017-2022 Bill Zissimopoulos 8 | */ 9 | /* 10 | * This file is part of Hubfs. 11 | * 12 | * You can redistribute it and/or modify it under the terms of the GNU 13 | * Affero General Public License version 3 as published by the Free 14 | * Software Foundation. 15 | */ 16 | 17 | package port 18 | 19 | import ( 20 | "syscall" 21 | 22 | "github.com/winfsp/cgofuse/fuse" 23 | ) 24 | 25 | func Setuidgid() func() { 26 | euid := syscall.Geteuid() 27 | if 0 == euid { 28 | uid, gid, _ := fuse.Getcontext() 29 | egid := syscall.Getegid() 30 | syscall.Setegid(int(gid)) 31 | syscall.Seteuid(int(uid)) 32 | return func() { 33 | syscall.Seteuid(euid) 34 | syscall.Setegid(egid) 35 | } 36 | } 37 | return func() { 38 | } 39 | } 40 | 41 | func copyFusestatfsFromGostatfs(dst *fuse.Statfs_t, src *syscall.Statfs_t) { 42 | *dst = fuse.Statfs_t{} 43 | dst.Bsize = uint64(src.Bsize) 44 | dst.Frsize = 1 45 | dst.Blocks = uint64(src.Blocks) 46 | dst.Bfree = uint64(src.Bfree) 47 | dst.Bavail = uint64(src.Bavail) 48 | dst.Files = uint64(src.Files) 49 | dst.Ffree = uint64(src.Ffree) 50 | dst.Favail = uint64(src.Ffree) 51 | dst.Namemax = 255 //uint64(src.Namelen) 52 | } 53 | 54 | func copyFusestatFromGostat(dst *fuse.Stat_t, src *syscall.Stat_t) { 55 | *dst = fuse.Stat_t{} 56 | dst.Dev = uint64(src.Dev) 57 | dst.Ino = uint64(src.Ino) 58 | dst.Mode = uint32(src.Mode) 59 | dst.Nlink = uint32(src.Nlink) 60 | dst.Uid = uint32(src.Uid) 61 | dst.Gid = uint32(src.Gid) 62 | dst.Rdev = uint64(src.Rdev) 63 | dst.Size = int64(src.Size) 64 | dst.Atim.Sec, dst.Atim.Nsec = src.Atimespec.Sec, src.Atimespec.Nsec 65 | dst.Mtim.Sec, dst.Mtim.Nsec = src.Mtimespec.Sec, src.Mtimespec.Nsec 66 | dst.Ctim.Sec, dst.Ctim.Nsec = src.Ctimespec.Sec, src.Ctimespec.Nsec 67 | dst.Blksize = int64(src.Blksize) 68 | dst.Blocks = int64(src.Blocks) 69 | dst.Birthtim.Sec, dst.Birthtim.Nsec = src.Birthtimespec.Sec, src.Birthtimespec.Nsec 70 | } 71 | -------------------------------------------------------------------------------- /src/fs/port/port_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | /* 5 | * port_linux.go 6 | * 7 | * Copyright 2017-2022 Bill Zissimopoulos 8 | */ 9 | /* 10 | * This file is part of Hubfs. 11 | * 12 | * You can redistribute it and/or modify it under the terms of the GNU 13 | * Affero General Public License version 3 as published by the Free 14 | * Software Foundation. 15 | */ 16 | 17 | package port 18 | 19 | import ( 20 | "syscall" 21 | 22 | "github.com/winfsp/cgofuse/fuse" 23 | ) 24 | 25 | func Setuidgid() func() { 26 | euid := syscall.Geteuid() 27 | if 0 == euid { 28 | uid, gid, _ := fuse.Getcontext() 29 | egid := syscall.Getegid() 30 | syscall.Setregid(-1, int(gid)) 31 | syscall.Setreuid(-1, int(uid)) 32 | return func() { 33 | syscall.Setreuid(-1, int(euid)) 34 | syscall.Setregid(-1, int(egid)) 35 | } 36 | } 37 | return func() { 38 | } 39 | } 40 | 41 | func copyFusestatfsFromGostatfs(dst *fuse.Statfs_t, src *syscall.Statfs_t) { 42 | *dst = fuse.Statfs_t{} 43 | dst.Bsize = uint64(src.Bsize) 44 | dst.Frsize = 1 45 | dst.Blocks = uint64(src.Blocks) 46 | dst.Bfree = uint64(src.Bfree) 47 | dst.Bavail = uint64(src.Bavail) 48 | dst.Files = uint64(src.Files) 49 | dst.Ffree = uint64(src.Ffree) 50 | dst.Favail = uint64(src.Ffree) 51 | dst.Namemax = 255 //uint64(src.Namelen) 52 | } 53 | 54 | func copyFusestatFromGostat(dst *fuse.Stat_t, src *syscall.Stat_t) { 55 | *dst = fuse.Stat_t{} 56 | dst.Dev = uint64(src.Dev) 57 | dst.Ino = uint64(src.Ino) 58 | dst.Mode = uint32(src.Mode) 59 | dst.Nlink = uint32(src.Nlink) 60 | dst.Uid = uint32(src.Uid) 61 | dst.Gid = uint32(src.Gid) 62 | dst.Rdev = uint64(src.Rdev) 63 | dst.Size = int64(src.Size) 64 | dst.Atim.Sec, dst.Atim.Nsec = src.Atim.Sec, src.Atim.Nsec 65 | dst.Mtim.Sec, dst.Mtim.Nsec = src.Mtim.Sec, src.Mtim.Nsec 66 | dst.Ctim.Sec, dst.Ctim.Nsec = src.Ctim.Sec, src.Ctim.Nsec 67 | dst.Blksize = int64(src.Blksize) 68 | dst.Blocks = int64(src.Blocks) 69 | } 70 | -------------------------------------------------------------------------------- /src/fs/port/port_unix.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || linux 2 | // +build darwin linux 3 | 4 | /* 5 | * port_unix.go 6 | * 7 | * Copyright 2017-2022 Bill Zissimopoulos 8 | */ 9 | /* 10 | * This file is part of Hubfs. 11 | * 12 | * You can redistribute it and/or modify it under the terms of the GNU 13 | * Affero General Public License version 3 as published by the Free 14 | * Software Foundation. 15 | */ 16 | 17 | package port 18 | 19 | import ( 20 | "path/filepath" 21 | "syscall" 22 | 23 | "github.com/winfsp/cgofuse/fuse" 24 | ) 25 | 26 | func Realpath(path string) (errc int, normpath string) { 27 | return Getpath(path) 28 | } 29 | 30 | func Chdir(path string) (errc int) { 31 | return Errno(syscall.Chdir(path)) 32 | } 33 | 34 | func Statfs(path string, stat *fuse.Statfs_t) (errc int) { 35 | gost := syscall.Statfs_t{} 36 | e := syscall.Statfs(path, &gost) 37 | if nil != e { 38 | return Errno(e) 39 | } 40 | copyFusestatfsFromGostatfs(stat, &gost) 41 | return 0 42 | } 43 | 44 | func Mknod(path string, mode uint32, dev int) (errc int) { 45 | return Errno(syscall.Mknod(path, mode, dev)) 46 | } 47 | 48 | func Mkdir(path string, mode uint32) (errc int) { 49 | return Errno(syscall.Mkdir(path, mode)) 50 | } 51 | 52 | func Unlink(path string) (errc int) { 53 | return Errno(syscall.Unlink(path)) 54 | } 55 | 56 | func Rmdir(path string) (errc int) { 57 | return Errno(syscall.Rmdir(path)) 58 | } 59 | 60 | func Link(oldpath string, newpath string) (errc int) { 61 | return Errno(syscall.Link(oldpath, newpath)) 62 | } 63 | 64 | func Symlink(oldpath string, newpath string) (errc int) { 65 | return Errno(syscall.Symlink(oldpath, newpath)) 66 | } 67 | 68 | func Readlink(path string) (errc int, target string) { 69 | buf := [4096]byte{} 70 | n, e := syscall.Readlink(path, buf[:]) 71 | if nil != e { 72 | return Errno(e), "" 73 | } 74 | return 0, string(buf[:n]) 75 | } 76 | 77 | func Rename(oldpath string, newpath string) (errc int) { 78 | return Errno(syscall.Rename(oldpath, newpath)) 79 | } 80 | 81 | func Chmod(path string, mode uint32) (errc int) { 82 | return Errno(syscall.Chmod(path, mode)) 83 | } 84 | 85 | func Lchown(path string, uid int, gid int) (errc int) { 86 | return Errno(syscall.Lchown(path, uid, gid)) 87 | } 88 | 89 | func Lchflags(path string, flags uint32) (errc int) { 90 | return -fuse.ENOSYS 91 | } 92 | 93 | func UtimesNano(path string, tmsp []fuse.Timespec) (errc int) { 94 | gots := [2]syscall.Timespec{} 95 | gots[0].Sec, gots[0].Nsec = tmsp[0].Sec, tmsp[0].Nsec 96 | gots[1].Sec, gots[1].Nsec = tmsp[1].Sec, tmsp[1].Nsec 97 | return Errno(syscall.UtimesNano(path, gots[:])) 98 | } 99 | 100 | func Open(path string, flags int, mode uint32) (errc int, fh uint64) { 101 | fd, e := syscall.Open(path, flags, mode) 102 | if nil != e { 103 | return Errno(e), ^uint64(0) 104 | } 105 | return 0, uint64(fd) 106 | } 107 | 108 | func Lstat(path string, stat *fuse.Stat_t) (errc int) { 109 | gost := syscall.Stat_t{} 110 | e := syscall.Lstat(path, &gost) 111 | if nil != e { 112 | return Errno(e) 113 | } 114 | copyFusestatFromGostat(stat, &gost) 115 | return 0 116 | } 117 | 118 | func Fstat(fh uint64, stat *fuse.Stat_t) (errc int) { 119 | gost := syscall.Stat_t{} 120 | e := syscall.Fstat(int(fh), &gost) 121 | if nil != e { 122 | return Errno(e) 123 | } 124 | copyFusestatFromGostat(stat, &gost) 125 | return 0 126 | } 127 | 128 | func Getpath(path string) (errc int, normpath string) { 129 | p, e := filepath.Abs(path) 130 | if nil != e { 131 | return Errno(e), "" 132 | } 133 | p, e = filepath.EvalSymlinks(p) 134 | if nil != e { 135 | return Errno(e), "" 136 | } 137 | return 0, p 138 | } 139 | 140 | func Fgetpath(fh uint64) (errc int, normpath string) { 141 | return -fuse.ENOSYS, "" 142 | } 143 | 144 | func Truncate(path string, length int64) (errc int) { 145 | return Errno(syscall.Truncate(path, length)) 146 | } 147 | 148 | func Ftruncate(fh uint64, length int64) (errc int) { 149 | return Errno(syscall.Ftruncate(int(fh), length)) 150 | } 151 | 152 | func Pread(fh uint64, p []byte, offset int64) (n int) { 153 | n, e := syscall.Pread(int(fh), p, offset) 154 | if nil != e { 155 | return Errno(e) 156 | } 157 | return n 158 | } 159 | 160 | func Pwrite(fh uint64, p []byte, offset int64) (n int) { 161 | n, e := syscall.Pwrite(int(fh), p, offset) 162 | if nil != e { 163 | return Errno(e) 164 | } 165 | return n 166 | } 167 | 168 | func Close(fh uint64) (errc int) { 169 | return Errno(syscall.Close(int(fh))) 170 | } 171 | 172 | func Fsync(fh uint64) (errc int) { 173 | return Errno(syscall.Fsync(int(fh))) 174 | } 175 | 176 | func Opendir(path string) (errc int, fh uint64) { 177 | fd, e := syscall.Open(path, syscall.O_RDONLY|syscall.O_DIRECTORY, 0) 178 | if nil != e { 179 | return Errno(e), ^uint64(0) 180 | } 181 | return 0, uint64(fd) 182 | } 183 | 184 | func Readdir(fh uint64, fill func(name string, stat *fuse.Stat_t, ofst int64) bool) (errc int) { 185 | buf := [8 * 1024]byte{} 186 | ptr := 0 187 | end := 0 188 | 189 | /* seek to beginning of directory in case file handle is being reused */ 190 | _, e := syscall.Seek(int(fh), 0, 0 /*SEEK_SET*/) 191 | if nil != e { 192 | return Errno(e) 193 | } 194 | 195 | for { 196 | if end <= ptr { 197 | ptr = 0 198 | var e error 199 | end, e = syscall.ReadDirent(int(fh), buf[:]) 200 | if nil != e { 201 | return Errno(e) 202 | } 203 | if 0 >= end { 204 | return 0 205 | } 206 | } 207 | 208 | n, _, names := syscall.ParseDirent(buf[ptr:end], -1, nil) 209 | ptr += n 210 | 211 | for _, name := range names { 212 | if !fill(name, nil, 0) { 213 | return 0 214 | } 215 | } 216 | } 217 | } 218 | 219 | func Closedir(fh uint64) (errc int) { 220 | return Errno(syscall.Close(int(fh))) 221 | } 222 | 223 | func Umask(mask int) (oldmask int) { 224 | return syscall.Umask(mask) 225 | } 226 | 227 | func Errno(err error) int { 228 | if nil == err { 229 | return 0 230 | } 231 | 232 | if e, ok := err.(syscall.Errno); ok { 233 | return -int(e) 234 | } 235 | 236 | return -fuse.EIO 237 | } 238 | -------------------------------------------------------------------------------- /src/fs/ptfs/ptfs.go: -------------------------------------------------------------------------------- 1 | //go:build windows || darwin || linux 2 | // +build windows darwin linux 3 | 4 | /* 5 | * ptfs.go 6 | * 7 | * Copyright 2017-2022 Bill Zissimopoulos 8 | */ 9 | /* 10 | * This file is part of Hubfs. 11 | * 12 | * You can redistribute it and/or modify it under the terms of the GNU 13 | * Affero General Public License version 3 as published by the Free 14 | * Software Foundation. 15 | */ 16 | 17 | package ptfs 18 | 19 | import ( 20 | "path/filepath" 21 | "runtime" 22 | "strings" 23 | 24 | "github.com/winfsp/cgofuse/fuse" 25 | "github.com/winfsp/hubfs/fs/port" 26 | ) 27 | 28 | type filesystem struct { 29 | fuse.FileSystemBase 30 | root string 31 | trimlen int 32 | } 33 | 34 | func (self *filesystem) Statfs(path string, stat *fuse.Statfs_t) (errc int) { 35 | path = filepath.Join(self.root, path) 36 | return port.Statfs(path, stat) 37 | } 38 | 39 | func (self *filesystem) Mknod(path string, mode uint32, dev uint64) (errc int) { 40 | defer port.Setuidgid()() 41 | path = filepath.Join(self.root, path) 42 | return port.Mknod(path, mode, int(dev)) 43 | } 44 | 45 | func (self *filesystem) Mkdir(path string, mode uint32) (errc int) { 46 | defer port.Setuidgid()() 47 | path = filepath.Join(self.root, path) 48 | return port.Mkdir(path, mode) 49 | } 50 | 51 | func (self *filesystem) Unlink(path string) (errc int) { 52 | path = filepath.Join(self.root, path) 53 | return port.Unlink(path) 54 | } 55 | 56 | func (self *filesystem) Rmdir(path string) (errc int) { 57 | path = filepath.Join(self.root, path) 58 | return port.Rmdir(path) 59 | } 60 | 61 | func (self *filesystem) Link(oldpath string, newpath string) (errc int) { 62 | defer port.Setuidgid()() 63 | oldpath = filepath.Join(self.root, oldpath) 64 | newpath = filepath.Join(self.root, newpath) 65 | return port.Link(oldpath, newpath) 66 | } 67 | 68 | func (self *filesystem) Symlink(target string, newpath string) (errc int) { 69 | defer port.Setuidgid()() 70 | newpath = filepath.Join(self.root, newpath) 71 | root := self.root 72 | if !strings.HasSuffix(root, string(filepath.Separator)) { 73 | root += string(filepath.Separator) 74 | } 75 | dest := filepath.Join(filepath.Dir(newpath), target) 76 | if !strings.HasPrefix(dest, root) { 77 | return -fuse.EPERM 78 | } 79 | return port.Symlink(target, newpath) 80 | } 81 | 82 | func (self *filesystem) Readlink(path string) (errc int, target string) { 83 | path = filepath.Join(self.root, path) 84 | return port.Readlink(path) 85 | } 86 | 87 | func (self *filesystem) Rename(oldpath string, newpath string) (errc int) { 88 | defer port.Setuidgid()() 89 | oldpath = filepath.Join(self.root, oldpath) 90 | newpath = filepath.Join(self.root, newpath) 91 | return port.Rename(oldpath, newpath) 92 | } 93 | 94 | func (self *filesystem) Chmod(path string, mode uint32) (errc int) { 95 | path = filepath.Join(self.root, path) 96 | return port.Chmod(path, mode) 97 | } 98 | 99 | func (self *filesystem) Chown(path string, uid uint32, gid uint32) (errc int) { 100 | path = filepath.Join(self.root, path) 101 | return port.Lchown(path, int(uid), int(gid)) 102 | } 103 | 104 | func (self *filesystem) Utimens(path string, tmsp []fuse.Timespec) (errc int) { 105 | path = filepath.Join(self.root, path) 106 | return port.UtimesNano(path, tmsp) 107 | } 108 | 109 | func (self *filesystem) Create(path string, flags int, mode uint32) (errc int, fh uint64) { 110 | defer port.Setuidgid()() 111 | path = filepath.Join(self.root, path) 112 | return port.Open(path, flags, mode) 113 | } 114 | 115 | func (self *filesystem) Open(path string, flags int) (errc int, fh uint64) { 116 | path = filepath.Join(self.root, path) 117 | return port.Open(path, flags, 0) 118 | } 119 | 120 | func (self *filesystem) Getattr(path string, stat *fuse.Stat_t, fh uint64) (errc int) { 121 | if ^uint64(0) == fh { 122 | if "windows" == runtime.GOOS { 123 | slashdot := strings.HasSuffix(path, "/.") 124 | path = filepath.Join(self.root, path) 125 | if slashdot { 126 | path += `\` 127 | } 128 | } else { 129 | path = filepath.Join(self.root, path) 130 | } 131 | return port.Lstat(path, stat) 132 | } else { 133 | return port.Fstat(fh, stat) 134 | } 135 | } 136 | 137 | func (self *filesystem) Getpath(path string, fh uint64) (errc int, normpath string) { 138 | if ^uint64(0) == fh { 139 | path = filepath.Join(self.root, path) 140 | errc, normpath = port.Getpath(path) 141 | } else { 142 | errc, normpath = port.Fgetpath(fh) 143 | } 144 | if 0 == errc { 145 | if len(normpath) > self.trimlen { 146 | normpath = normpath[self.trimlen:] 147 | } else { 148 | normpath = "/" 149 | } 150 | } 151 | return 152 | } 153 | 154 | func (self *filesystem) Truncate(path string, size int64, fh uint64) (errc int) { 155 | if ^uint64(0) == fh { 156 | path = filepath.Join(self.root, path) 157 | errc = port.Truncate(path, size) 158 | } else { 159 | errc = port.Ftruncate(fh, size) 160 | } 161 | return 162 | } 163 | 164 | func (self *filesystem) Read(path string, buff []byte, ofst int64, fh uint64) (n int) { 165 | return port.Pread(fh, buff, ofst) 166 | } 167 | 168 | func (self *filesystem) Write(path string, buff []byte, ofst int64, fh uint64) (n int) { 169 | return port.Pwrite(fh, buff, ofst) 170 | } 171 | 172 | func (self *filesystem) Release(path string, fh uint64) (errc int) { 173 | return port.Close(fh) 174 | } 175 | 176 | func (self *filesystem) Fsync(path string, datasync bool, fh uint64) (errc int) { 177 | return port.Fsync(fh) 178 | } 179 | 180 | func (self *filesystem) Opendir(path string) (errc int, fh uint64) { 181 | path = filepath.Join(self.root, path) 182 | return port.Opendir(path) 183 | } 184 | 185 | func (self *filesystem) Readdir(path string, 186 | fill func(name string, stat *fuse.Stat_t, ofst int64) bool, 187 | ofst int64, 188 | fh uint64) (errc int) { 189 | return port.Readdir(fh, fill) 190 | } 191 | 192 | func (self *filesystem) Releasedir(path string, fh uint64) (errc int) { 193 | return port.Closedir(fh) 194 | } 195 | 196 | func (self *filesystem) Chflags(path string, flags uint32) (errc int) { 197 | path = filepath.Join(self.root, path) 198 | return port.Lchflags(path, flags) 199 | } 200 | 201 | func (self *filesystem) Setcrtime(path string, tmsp fuse.Timespec) (errc int) { 202 | path = filepath.Join(self.root, path) 203 | arts := [4]fuse.Timespec{} 204 | arts[3] = tmsp 205 | return port.UtimesNano(path, arts[:]) 206 | } 207 | 208 | func New(root string) fuse.FileSystemInterface { 209 | trimlen := 0 210 | if "windows" == runtime.GOOS { 211 | volname := filepath.VolumeName(root) 212 | trimlen = len(root) - len(volname) 213 | if 1 == trimlen { 214 | trimlen = 0 215 | } 216 | } 217 | return &filesystem{ 218 | root: root, 219 | trimlen: trimlen, 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/fs/unionfs/filemap.go: -------------------------------------------------------------------------------- 1 | /* 2 | * filemap.go 3 | * 4 | * Copyright 2021-2022 Bill Zissimopoulos 5 | */ 6 | /* 7 | * This file is part of Hubfs. 8 | * 9 | * You can redistribute it and/or modify it under the terms of the GNU 10 | * Affero General Public License version 3 as published by the Free 11 | * Software Foundation. 12 | */ 13 | 14 | package unionfs 15 | 16 | type Filer interface { 17 | CopyFile(path string, file interface{}) bool 18 | ReopenFile(oldpath string, newpath string, file interface{}) 19 | } 20 | 21 | type Filemap struct { 22 | Filer 23 | Caseins bool 24 | 25 | openmap map[uint64]*fileitem 26 | pathmap map[Pathkey]*fileitem 27 | nextfh uint64 28 | } 29 | 30 | type fileitem struct { 31 | prev, next *fileitem 32 | file interface{} 33 | } 34 | 35 | func NewFilemap(filer Filer, caseins bool) (fm *Filemap) { 36 | fm = &Filemap{ 37 | Filer: filer, 38 | Caseins: caseins, 39 | openmap: make(map[uint64]*fileitem), 40 | pathmap: make(map[Pathkey]*fileitem), 41 | } 42 | return 43 | } 44 | 45 | func (fm *Filemap) NewFile(path string, file interface{}, track bool) (fh uint64) { 46 | for { 47 | fh = fm.nextfh 48 | fm.nextfh++ 49 | _, ok := fm.openmap[fh] 50 | if !ok && ^uint64(0) != fh { 51 | break 52 | } 53 | } 54 | 55 | f := &fileitem{file: file} 56 | f.prev = f 57 | f.next = f 58 | fm.openmap[fh] = f 59 | 60 | if track { 61 | k := ComputePathkey(path, fm.Caseins) 62 | l, ok := fm.pathmap[k] 63 | if !ok { 64 | l = &fileitem{} 65 | l.prev = l 66 | l.next = l 67 | fm.pathmap[k] = l 68 | } 69 | p := l.prev 70 | f.next = l 71 | f.prev = p 72 | p.next = f 73 | l.prev = f 74 | } 75 | 76 | return 77 | } 78 | 79 | func (fm *Filemap) DelFile(path string, fh uint64) { 80 | f, ok := fm.openmap[fh] 81 | if ok { 82 | n := f.next 83 | p := f.prev 84 | n.prev = p 85 | p.next = n 86 | delete(fm.openmap, fh) 87 | 88 | if n != f { 89 | k := ComputePathkey(path, fm.Caseins) 90 | l, ok := fm.pathmap[k] 91 | if ok && l.next == l { 92 | delete(fm.pathmap, k) 93 | } 94 | } 95 | } 96 | } 97 | 98 | func (fm *Filemap) GetFile(path string, fh uint64, okreset bool) (file interface{}) { 99 | f, ok := fm.openmap[fh] 100 | if ok { 101 | if okreset && fm.Filer.CopyFile(path, f.file) { 102 | f, ok = fm.openmap[fh] 103 | if ok { 104 | file = f.file 105 | } 106 | } else { 107 | file = f.file 108 | } 109 | } 110 | 111 | return 112 | } 113 | 114 | func (fm *Filemap) Remove(path string) { 115 | k := ComputePathkey(path, fm.Caseins) 116 | l, ok := fm.pathmap[k] 117 | if ok { 118 | for f := l.next; l != f; { 119 | fm.Filer.ReopenFile(path, path, f.file) 120 | n := f.next 121 | f.prev = f 122 | f.next = f 123 | f = n 124 | } 125 | 126 | delete(fm.pathmap, k) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/fs/unionfs/pathkey.go: -------------------------------------------------------------------------------- 1 | /* 2 | * pathkey.go 3 | * 4 | * Copyright 2021-2022 Bill Zissimopoulos 5 | */ 6 | /* 7 | * This file is part of Hubfs. 8 | * 9 | * You can redistribute it and/or modify it under the terms of the GNU 10 | * Affero General Public License version 3 as published by the Free 11 | * Software Foundation. 12 | */ 13 | 14 | package unionfs 15 | 16 | import ( 17 | "crypto/sha256" 18 | "hash" 19 | "strings" 20 | ) 21 | 22 | const Pathkeylen = 16 23 | 24 | type Pathkey [Pathkeylen]uint8 25 | 26 | // Function ComputePathkey computes the path key for a path. 27 | func ComputePathkey(path string, caseins bool) (k Pathkey) { 28 | if caseins { 29 | path = strings.ToUpper(path) 30 | } 31 | sum := sha256.Sum256([]uint8(path)) 32 | copy(k[1:], sum[:]) 33 | return 34 | } 35 | 36 | type PathkeyHash struct { 37 | hash.Hash 38 | caseins bool 39 | } 40 | 41 | func NewPathkeyHash(caseins bool) PathkeyHash { 42 | return PathkeyHash{sha256.New(), caseins} 43 | } 44 | 45 | func (h PathkeyHash) Write(s string) { 46 | if h.caseins { 47 | s = strings.ToUpper(s) 48 | } 49 | h.Hash.Write([]uint8(s)) 50 | } 51 | 52 | func (h PathkeyHash) ComputePathkey() (k Pathkey) { 53 | copy(k[1:], h.Hash.Sum(nil)) 54 | return 55 | } 56 | -------------------------------------------------------------------------------- /src/fs/unionfs/pathkey_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * pathkey_test.go 3 | * 4 | * Copyright 2021-2022 Bill Zissimopoulos 5 | */ 6 | /* 7 | * This file is part of Hubfs. 8 | * 9 | * You can redistribute it and/or modify it under the terms of the GNU 10 | * Affero General Public License version 3 as published by the Free 11 | * Software Foundation. 12 | */ 13 | 14 | package unionfs 15 | 16 | import ( 17 | "testing" 18 | ) 19 | 20 | func TestPathkeyCompute(t *testing.T) { 21 | var k Pathkey 22 | var k0 = Pathkey{ 23 | 0x00, 0x8a, 0x5e, 0xda, 0xb2, 0x82, 0x63, 0x24, 24 | 0x43, 0x21, 0x9e, 0x05, 0x1e, 0x4a, 0xde, 0x2d, 25 | } 26 | var k1 = Pathkey{ 27 | 0x00, 0x37, 0x9c, 0x9f, 0x23, 0x42, 0x5a, 0x38, 28 | 0x69, 0x8d, 0x16, 0x4a, 0xbe, 0xb3, 0x39, 0x11, 29 | } 30 | 31 | k = ComputePathkey("/", false) 32 | if k0 != k { 33 | t.Error() 34 | } 35 | 36 | k = ComputePathkey("/path", false) 37 | if k1 != k { 38 | t.Error() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/fs/unionfs/pathmap_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * pathmap_test.go 3 | * 4 | * Copyright 2021-2022 Bill Zissimopoulos 5 | */ 6 | /* 7 | * This file is part of Hubfs. 8 | * 9 | * You can redistribute it and/or modify it under the terms of the GNU 10 | * Affero General Public License version 3 as published by the Free 11 | * Software Foundation. 12 | */ 13 | 14 | package unionfs 15 | 16 | import ( 17 | "fmt" 18 | "reflect" 19 | "testing" 20 | ) 21 | 22 | func TestPathmapOpenClose(t *testing.T) { 23 | fs := newTestfs() 24 | 25 | ec, pm := OpenPathmap(fs, "/.pathmap$", false) 26 | if 0 != ec { 27 | t.Error() 28 | } 29 | pm.Close() 30 | 31 | ec, pm = OpenPathmap(fs, "/.pathmap$", false) 32 | if 0 != ec { 33 | t.Error() 34 | } 35 | pm.Close() 36 | } 37 | 38 | func TestPathmapGetSet(t *testing.T) { 39 | fs := newTestfs() 40 | 41 | ec, pm := OpenPathmap(fs, "/.pathmap$", false) 42 | if 0 != ec { 43 | t.Error() 44 | } 45 | defer pm.Close() 46 | 47 | isopq, v := false, UNKNOWN 48 | 49 | pm.Set("/a/bb/ccc", 42) 50 | isopq, v = pm.Get("/a") 51 | if false != isopq || UNKNOWN != v { 52 | t.Error() 53 | } 54 | isopq, v = pm.Get("/a/bb") 55 | if false != isopq || UNKNOWN != v { 56 | t.Error() 57 | } 58 | isopq, v = pm.Get("/a/bb/ccc") 59 | if false != isopq || 42 != v { 60 | t.Error() 61 | } 62 | 63 | pm.Set("/a/bb", 43) 64 | isopq, v = pm.Get("/a") 65 | if false != isopq || UNKNOWN != v { 66 | t.Error() 67 | } 68 | isopq, v = pm.Get("/a/bb") 69 | if false != isopq || 43 != v { 70 | t.Error() 71 | } 72 | isopq, v = pm.Get("/a/bb/ccc") 73 | if false != isopq || 42 != v { 74 | t.Error() 75 | } 76 | 77 | pm.Set("/a/b", 44) 78 | isopq, v = pm.Get("/a") 79 | if false != isopq || UNKNOWN != v { 80 | t.Error() 81 | } 82 | isopq, v = pm.Get("/a/b") 83 | if false != isopq || 44 != v { 84 | t.Error() 85 | } 86 | 87 | pm.Set("/a/bb/ccc", NOTEXIST) 88 | pm.Set("/a/bb", NOTEXIST) 89 | isopq, v = pm.Get("/a") 90 | if false != isopq || UNKNOWN != v { 91 | t.Error() 92 | } 93 | isopq, v = pm.Get("/a/bb") 94 | if false != isopq || NOTEXIST != v { 95 | t.Error() 96 | } 97 | isopq, v = pm.Get("/a/bb/ccc") 98 | if false != isopq || NOTEXIST != v { 99 | t.Error() 100 | } 101 | } 102 | 103 | func TestPathmapGetSetOpaque(t *testing.T) { 104 | fs := newTestfs() 105 | 106 | ec, pm := OpenPathmap(fs, "/.pathmap$", false) 107 | if 0 != ec { 108 | t.Error() 109 | } 110 | defer pm.Close() 111 | 112 | isopq, v := false, UNKNOWN 113 | 114 | pm.Set("/a/bb/ccc", 42) 115 | isopq, v = pm.Get("/a") 116 | if false != isopq || UNKNOWN != v { 117 | t.Error() 118 | } 119 | isopq, v = pm.Get("/a/bb") 120 | if false != isopq || UNKNOWN != v { 121 | t.Error() 122 | } 123 | isopq, v = pm.Get("/a/bb/ccc") 124 | if false != isopq || 42 != v { 125 | t.Error() 126 | } 127 | 128 | pm.Set("/a/bb", OPAQUE) 129 | isopq, v = pm.Get("/a") 130 | if false != isopq || UNKNOWN != v { 131 | t.Error() 132 | } 133 | isopq, v = pm.Get("/a/bb") 134 | if true != isopq || 0 != v { 135 | t.Error() 136 | } 137 | isopq, v = pm.Get("/a/bb/ccc") 138 | if true != isopq || 42 != v { 139 | t.Error() 140 | } 141 | } 142 | 143 | func TestPathmapWriteIncremental(t *testing.T) { 144 | fs := newTestfs() 145 | 146 | ec, pm := OpenPathmap(fs, "/.pathmap$", false) 147 | if 0 != ec { 148 | t.Error() 149 | } 150 | defer pm.Close() 151 | 152 | isopq, v := false, UNKNOWN 153 | 154 | pm.Set("/a/bb/ccc", 42) 155 | isopq, v = pm.Get("/a") 156 | if false != isopq || UNKNOWN != v { 157 | t.Error() 158 | } 159 | isopq, v = pm.Get("/a/bb") 160 | if false != isopq || UNKNOWN != v { 161 | t.Error() 162 | } 163 | isopq, v = pm.Get("/a/bb/ccc") 164 | if false != isopq || 42 != v { 165 | t.Error() 166 | } 167 | 168 | n := pm.Write(false) 169 | if 0 > n { 170 | t.Error() 171 | } 172 | 173 | ec, pm2 := OpenPathmap(fs, "/.pathmap$", false) 174 | if 0 != ec { 175 | t.Error() 176 | } 177 | isopq, v = pm2.Get("/a") 178 | if false != isopq || UNKNOWN != v { 179 | t.Error() 180 | } 181 | isopq, v = pm2.Get("/a/bb") 182 | if false != isopq || UNKNOWN != v { 183 | t.Error() 184 | } 185 | isopq, v = pm2.Get("/a/bb/ccc") 186 | if false != isopq || UNKNOWN != v { 187 | t.Error() 188 | } 189 | pm2.Close() 190 | 191 | n = pm.Write(false) 192 | if 0 > n { 193 | t.Error() 194 | } 195 | 196 | ec, pm2 = OpenPathmap(fs, "/.pathmap$", false) 197 | if 0 != ec { 198 | t.Error() 199 | } 200 | isopq, v = pm2.Get("/a") 201 | if false != isopq || UNKNOWN != v { 202 | t.Error() 203 | } 204 | isopq, v = pm2.Get("/a/bb") 205 | if false != isopq || UNKNOWN != v { 206 | t.Error() 207 | } 208 | isopq, v = pm2.Get("/a/bb/ccc") 209 | if false != isopq || UNKNOWN != v { 210 | t.Error() 211 | } 212 | pm2.Close() 213 | 214 | pm.Set("/a/b/c", 43) 215 | isopq, v = pm.Get("/a") 216 | if false != isopq || UNKNOWN != v { 217 | t.Error() 218 | } 219 | isopq, v = pm.Get("/a/bb") 220 | if false != isopq || UNKNOWN != v { 221 | t.Error() 222 | } 223 | isopq, v = pm.Get("/a/bb/ccc") 224 | if false != isopq || 42 != v { 225 | t.Error() 226 | } 227 | isopq, v = pm.Get("/a/b") 228 | if false != isopq || UNKNOWN != v { 229 | t.Error() 230 | } 231 | isopq, v = pm.Get("/a/b/c") 232 | if false != isopq || 43 != v { 233 | t.Error() 234 | } 235 | 236 | n = pm.Write(false) 237 | if 0 > n { 238 | t.Error() 239 | } 240 | 241 | ec, pm2 = OpenPathmap(fs, "/.pathmap$", false) 242 | if 0 != ec { 243 | t.Error() 244 | } 245 | isopq, v = pm2.Get("/a") 246 | if false != isopq || UNKNOWN != v { 247 | t.Error() 248 | } 249 | isopq, v = pm2.Get("/a/bb") 250 | if false != isopq || UNKNOWN != v { 251 | t.Error() 252 | } 253 | isopq, v = pm2.Get("/a/b") 254 | if false != isopq || UNKNOWN != v { 255 | t.Error() 256 | } 257 | pm2.Close() 258 | 259 | pm.Set("/a/b/c", WHITEOUT) 260 | pm.Set("/a/b", 50) 261 | isopq, v = pm.Get("/a") 262 | if false != isopq || UNKNOWN != v { 263 | t.Error() 264 | } 265 | isopq, v = pm.Get("/a/bb") 266 | if false != isopq || UNKNOWN != v { 267 | t.Error() 268 | } 269 | isopq, v = pm.Get("/a/bb/ccc") 270 | if false != isopq || 42 != v { 271 | t.Error() 272 | } 273 | isopq, v = pm.Get("/a/b") 274 | if false != isopq || 50 != v { 275 | t.Error() 276 | } 277 | isopq, v = pm.Get("/a/b/c") 278 | if false != isopq || WHITEOUT != v { 279 | t.Error() 280 | } 281 | 282 | n = pm.Write(false) 283 | if 0 > n { 284 | t.Error() 285 | } 286 | 287 | ec, pm2 = OpenPathmap(fs, "/.pathmap$", false) 288 | if 0 != ec { 289 | t.Error() 290 | } 291 | isopq, v = pm2.Get("/a") 292 | if false != isopq || UNKNOWN != v { 293 | t.Error() 294 | } 295 | isopq, v = pm2.Get("/a/bb") 296 | if false != isopq || UNKNOWN != v { 297 | t.Error() 298 | } 299 | isopq, v = pm2.Get("/a/bb/ccc") 300 | if false != isopq || UNKNOWN != v { 301 | t.Error() 302 | } 303 | isopq, v = pm2.Get("/a/b") 304 | if false != isopq || UNKNOWN != v { 305 | t.Error() 306 | } 307 | isopq, v = pm2.Get("/a/b/c") 308 | if false != isopq || WHITEOUT != v { 309 | t.Error() 310 | } 311 | pm2.Close() 312 | 313 | pm.Set("/a/b/c", 0) 314 | isopq, v = pm.Get("/a") 315 | if false != isopq || UNKNOWN != v { 316 | t.Error() 317 | } 318 | isopq, v = pm.Get("/a/bb") 319 | if false != isopq || UNKNOWN != v { 320 | t.Error() 321 | } 322 | isopq, v = pm.Get("/a/bb/ccc") 323 | if false != isopq || 42 != v { 324 | t.Error() 325 | } 326 | isopq, v = pm.Get("/a/b") 327 | if false != isopq || 50 != v { 328 | t.Error() 329 | } 330 | isopq, v = pm.Get("/a/b/c") 331 | if false != isopq || 0 != v { 332 | t.Error() 333 | } 334 | 335 | n = pm.Write(false) 336 | if 0 > n { 337 | t.Error() 338 | } 339 | 340 | ec, pm2 = OpenPathmap(fs, "/.pathmap$", false) 341 | if 0 != ec { 342 | t.Error() 343 | } 344 | isopq, v = pm2.Get("/a") 345 | if false != isopq || UNKNOWN != v { 346 | t.Error() 347 | } 348 | isopq, v = pm2.Get("/a/bb") 349 | if false != isopq || UNKNOWN != v { 350 | t.Error() 351 | } 352 | isopq, v = pm2.Get("/a/bb/ccc") 353 | if false != isopq || UNKNOWN != v { 354 | t.Error() 355 | } 356 | isopq, v = pm2.Get("/a/b") 357 | if false != isopq || UNKNOWN != v { 358 | t.Error() 359 | } 360 | isopq, v = pm2.Get("/a/b/c") 361 | if false != isopq || UNKNOWN != v { 362 | t.Error() 363 | } 364 | pm2.Close() 365 | } 366 | 367 | func TestPathmapWrite(t *testing.T) { 368 | fs := newTestfs() 369 | 370 | ec, pm := OpenPathmap(fs, "/.pathmap$", false) 371 | if 0 != ec { 372 | t.Error() 373 | } 374 | defer pm.Close() 375 | 376 | N := 10000 377 | 378 | for i := 0; N > i; i++ { 379 | path := fmt.Sprintf("/%v", i) 380 | 381 | pm.Set(path, OPAQUE) 382 | isopq, v := pm.Get(path) 383 | if true != isopq || 0 != v { 384 | t.Error() 385 | } 386 | 387 | if 0 == (i+1)%(N/10) { 388 | n := pm.Write(false) 389 | if 0 > n { 390 | t.Error() 391 | } 392 | 393 | ec, pm2 := OpenPathmap(fs, "/.pathmap$", false) 394 | if 0 != ec { 395 | t.Error() 396 | } 397 | if !reflect.DeepEqual(pm.vm, pm2.vm) { 398 | t.Error() 399 | } 400 | pm2.Close() 401 | } 402 | } 403 | 404 | for i := 0; N > i; i++ { 405 | path := fmt.Sprintf("/%v", i) 406 | 407 | pm.Set(path, 42) 408 | isopq, v := pm.Get(path) 409 | if false != isopq || 42 != v { 410 | t.Error() 411 | } 412 | 413 | if 0 == (i+1)%(N/10) { 414 | n := pm.Write(false) 415 | if 0 > n { 416 | t.Error() 417 | } 418 | 419 | ec, pm2 := OpenPathmap(fs, "/.pathmap$", false) 420 | if 0 != ec { 421 | t.Error() 422 | } 423 | if len(pm2.vm) != N-i-1 { 424 | t.Error() 425 | } 426 | pm2.Close() 427 | } 428 | } 429 | 430 | for i := 0; N > i; i++ { 431 | path := fmt.Sprintf("/%v", i) 432 | 433 | pm.Set(path, WHITEOUT) 434 | isopq, v := pm.Get(path) 435 | if false != isopq || WHITEOUT != v { 436 | t.Error() 437 | } 438 | 439 | if 0 == (i+1)%(N/2) { 440 | n := pm.Write(false) 441 | if 0 > n { 442 | t.Error() 443 | } 444 | 445 | ec, pm2 := OpenPathmap(fs, "/.pathmap$", false) 446 | if 0 != ec { 447 | t.Error() 448 | } 449 | if len(pm2.vm) != i+1 { 450 | t.Error() 451 | } 452 | pm2.Close() 453 | } 454 | } 455 | 456 | ec, pm2 := OpenPathmap(fs, "/.pathmap$", false) 457 | if 0 != ec { 458 | t.Error() 459 | } 460 | if !reflect.DeepEqual(pm.vm, pm2.vm) { 461 | t.Error() 462 | } 463 | pm2.Close() 464 | } 465 | 466 | func TestPathmapPurge(t *testing.T) { 467 | fs := newTestfs() 468 | 469 | ec, pm := OpenPathmap(fs, "/.pathmap$", false) 470 | if 0 != ec { 471 | t.Error() 472 | } 473 | defer pm.Close() 474 | 475 | isopq, v := false, UNKNOWN 476 | 477 | pm.Set("/a/bb", OPAQUE) 478 | pm.Set("/a/bb/ccc", 42) 479 | isopq, v = pm.Get("/a") 480 | if false != isopq || UNKNOWN != v { 481 | t.Error() 482 | } 483 | isopq, v = pm.Get("/a/bb") 484 | if true != isopq || 0 != v { 485 | t.Error() 486 | } 487 | isopq, v = pm.Get("/a/bb/ccc") 488 | if true != isopq || 42 != v { 489 | t.Error() 490 | } 491 | 492 | n := pm.Write(false) 493 | if 0 > n { 494 | t.Error() 495 | } 496 | 497 | pm.Purge() 498 | 499 | isopq, v = pm.Get("/a") 500 | if false != isopq || UNKNOWN != v { 501 | t.Error() 502 | } 503 | isopq, v = pm.Get("/a/bb") 504 | if true != isopq || 0 != v { 505 | t.Error() 506 | } 507 | isopq, v = pm.Get("/a/bb/ccc") 508 | if true != isopq || UNKNOWN != v { 509 | t.Error() 510 | } 511 | 512 | if 1 != len(pm.vm) { 513 | t.Error() 514 | } 515 | 516 | ec, pm2 := OpenPathmap(fs, "/.pathmap$", false) 517 | if 0 != ec { 518 | t.Error() 519 | } 520 | if !reflect.DeepEqual(pm.vm, pm2.vm) { 521 | t.Error() 522 | } 523 | pm2.Close() 524 | } 525 | -------------------------------------------------------------------------------- /src/fs/unionfs/testfs_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * memfs_test.go 3 | * 4 | * Copyright 2021-2022 Bill Zissimopoulos 5 | */ 6 | /* 7 | * This file is part of Hubfs. 8 | * 9 | * You can redistribute it and/or modify it under the terms of the GNU 10 | * Affero General Public License version 3 as published by the Free 11 | * Software Foundation. 12 | */ 13 | 14 | package unionfs 15 | 16 | import ( 17 | "github.com/winfsp/cgofuse/fuse" 18 | "github.com/winfsp/hubfs/fs/memfs" 19 | ) 20 | 21 | func newTestfs() fuse.FileSystemInterface { 22 | fuse.OptParse([]string{}, "") 23 | 24 | return memfs.New() 25 | } 26 | -------------------------------------------------------------------------------- /src/git/git.go: -------------------------------------------------------------------------------- 1 | /* 2 | * git.go 3 | * 4 | * Copyright 2021-2022 Bill Zissimopoulos 5 | */ 6 | /* 7 | * This file is part of Hubfs. 8 | * 9 | * You can redistribute it and/or modify it under the terms of the GNU 10 | * Affero General Public License version 3 as published by the Free 11 | * Software Foundation. 12 | */ 13 | 14 | package git 15 | 16 | import ( 17 | "context" 18 | "io" 19 | "time" 20 | 21 | libtrace "github.com/billziss-gh/golib/trace" 22 | "github.com/go-git/go-git/v5/plumbing" 23 | "github.com/go-git/go-git/v5/plumbing/format/packfile" 24 | "github.com/go-git/go-git/v5/plumbing/object" 25 | "github.com/go-git/go-git/v5/plumbing/protocol/packp" 26 | "github.com/go-git/go-git/v5/plumbing/protocol/packp/sideband" 27 | "github.com/go-git/go-git/v5/plumbing/storer" 28 | "github.com/go-git/go-git/v5/plumbing/transport" 29 | "github.com/go-git/go-git/v5/plumbing/transport/http" 30 | "github.com/winfsp/hubfs/httputil" 31 | ) 32 | 33 | type ObjectType int 34 | 35 | const ( 36 | CommitObject ObjectType = 1 37 | TreeObject ObjectType = 2 38 | BlobObject ObjectType = 3 39 | TagObject ObjectType = 4 40 | ) 41 | 42 | type Repository struct { 43 | session transport.UploadPackSession 44 | advrefs *packp.AdvRefs 45 | } 46 | 47 | type Signature struct { 48 | Name string 49 | Email string 50 | Time time.Time 51 | } 52 | 53 | type Tag struct { 54 | Tagger Signature 55 | TargetHash string 56 | } 57 | 58 | type Commit struct { 59 | Author Signature 60 | Committer Signature 61 | TreeHash string 62 | } 63 | 64 | type TreeEntry struct { 65 | Name string 66 | Mode uint32 67 | Hash string 68 | } 69 | 70 | func OpenRepository(remote string, username string, password string) (res *Repository, err error) { 71 | endpoint, err := transport.NewEndpoint(remote) 72 | if nil != err { 73 | return nil, err 74 | } 75 | 76 | var auth transport.AuthMethod 77 | if "" != username || "" != password { 78 | auth = &http.BasicAuth{ 79 | Username: username, 80 | Password: password, 81 | } 82 | } 83 | 84 | client := http.NewClient(httputil.DefaultClient) 85 | session, err := client.NewUploadPackSession(endpoint, auth) 86 | if nil != err { 87 | return nil, err 88 | } 89 | 90 | advrefs, err := session.AdvertisedReferences() 91 | if nil != err { 92 | session.Close() 93 | return nil, err 94 | } 95 | 96 | return &Repository{ 97 | session: session, 98 | advrefs: advrefs, 99 | }, nil 100 | } 101 | 102 | func (repository *Repository) Close() (err error) { 103 | return repository.session.Close() 104 | } 105 | 106 | func (repository *Repository) GetRefs() (res map[string]string, err error) { 107 | stg, err := repository.advrefs.AllReferences() 108 | if nil != err { 109 | return nil, err 110 | } 111 | 112 | res = make(map[string]string, len(stg)) 113 | for n, r := range stg { 114 | res[string(n)] = r.Hash().String() 115 | } 116 | 117 | return res, nil 118 | } 119 | 120 | type storemap map[plumbing.Hash]plumbing.EncodedObject 121 | 122 | func (m storemap) NewEncodedObject() plumbing.EncodedObject { 123 | return &plumbing.MemoryObject{} 124 | } 125 | 126 | func (m storemap) SetEncodedObject(obj plumbing.EncodedObject) (plumbing.Hash, error) { 127 | hash := obj.Hash() 128 | m[hash] = obj 129 | return hash, nil 130 | } 131 | 132 | func (m storemap) EncodedObject(typ plumbing.ObjectType, hash plumbing.Hash) ( 133 | plumbing.EncodedObject, error) { 134 | obj, ok := m[hash] 135 | if !ok || (plumbing.AnyObject != typ && obj.Type() != typ) { 136 | return nil, plumbing.ErrObjectNotFound 137 | } 138 | 139 | return obj, nil 140 | } 141 | 142 | func (m storemap) IterEncodedObjects(typ plumbing.ObjectType) (storer.EncodedObjectIter, error) { 143 | lst := make([]plumbing.EncodedObject, 0, len(m)) 144 | for _, obj := range m { 145 | if plumbing.AnyObject == typ || obj.Type() == typ { 146 | lst = append(lst, obj) 147 | } 148 | } 149 | return storer.NewEncodedObjectSliceIter(lst), nil 150 | } 151 | 152 | func (m storemap) HasEncodedObject(hash plumbing.Hash) error { 153 | _, ok := m[hash] 154 | if !ok { 155 | return plumbing.ErrObjectNotFound 156 | } 157 | return nil 158 | } 159 | 160 | func (m storemap) EncodedObjectSize(hash plumbing.Hash) (int64, error) { 161 | obj, ok := m[hash] 162 | if !ok { 163 | return 0, plumbing.ErrObjectNotFound 164 | } 165 | return obj.Size(), nil 166 | } 167 | 168 | type observer struct { 169 | fn func(hash string, ot ObjectType, content []byte) error 170 | ot ObjectType 171 | } 172 | 173 | func (obs *observer) OnHeader(count uint32) error { 174 | return nil 175 | } 176 | 177 | func (obs *observer) OnInflatedObjectHeader(ot plumbing.ObjectType, objSize int64, pos int64) error { 178 | obs.ot = ObjectType(ot) 179 | return nil 180 | } 181 | 182 | func (obs *observer) OnInflatedObjectContent(h plumbing.Hash, pos int64, crc uint32, content []byte) error { 183 | return obs.fn(h.String(), obs.ot, content) 184 | } 185 | 186 | func (obs *observer) OnFooter(h plumbing.Hash) error { 187 | return nil 188 | } 189 | 190 | func (repository *Repository) fetchObjects(wants []string, 191 | fn func(hash string, ot ObjectType, content []byte) error) (err error) { 192 | defer trace(len(wants))(&err) 193 | 194 | req := packp.NewUploadPackRequestFromCapabilities(repository.advrefs.Capabilities) 195 | 196 | if nil == req.Capabilities.Set("shallow") { 197 | req.Depth = packp.DepthCommits(1) 198 | } 199 | if repository.advrefs.Capabilities.Supports("no-progress") { 200 | req.Capabilities.Set("no-progress") 201 | } 202 | if repository.advrefs.Capabilities.Supports("filter") { 203 | req.Capabilities.Set("filter") 204 | req.Filter = "tree:0" 205 | } 206 | 207 | req.Wants = make([]plumbing.Hash, len(wants)) 208 | for i, w := range wants { 209 | req.Wants[i] = plumbing.NewHash(w) 210 | } 211 | 212 | rsp, err := repository.session.UploadPack(context.Background(), req) 213 | if nil != err { 214 | return err 215 | } 216 | defer rsp.Close() 217 | 218 | var reader io.Reader 219 | switch { 220 | case req.Capabilities.Supports("side-band-64k"): 221 | reader = sideband.NewDemuxer(sideband.Sideband64k, rsp) 222 | case req.Capabilities.Supports("side-band"): 223 | reader = sideband.NewDemuxer(sideband.Sideband, rsp) 224 | default: 225 | reader = rsp 226 | } 227 | 228 | scn := packfile.NewScanner(reader) 229 | stg := storemap{} 230 | obs := &observer{fn: fn} 231 | parser, err := packfile.NewParserWithStorage(scn, stg, obs) 232 | if nil != err { 233 | return err 234 | } 235 | 236 | _, err = parser.Parse() 237 | if nil != err { 238 | return err 239 | } 240 | 241 | return nil 242 | } 243 | 244 | func (repository *Repository) FetchObjects(wants []string, 245 | fn func(hash string, ot ObjectType, content []byte) error) (err error) { 246 | 247 | for i, j := 0, 0; len(wants) > i; i = j { 248 | j = i + 256 249 | if len(wants) < j { 250 | j = len(wants) 251 | } 252 | err = repository.fetchObjects(wants[i:j], fn) 253 | if nil != err { 254 | return err 255 | } 256 | } 257 | 258 | return nil 259 | } 260 | 261 | func DecodeTag(content []byte) (res *Tag, err error) { 262 | obj := &plumbing.MemoryObject{} 263 | obj.SetType(plumbing.TagObject) 264 | obj.Write(content) 265 | t := &object.Tag{} 266 | err = t.Decode(obj) 267 | if nil != err { 268 | return 269 | } 270 | if plumbing.CommitObject != t.TargetType { 271 | err = plumbing.ErrInvalidType 272 | return 273 | } 274 | res = &Tag{ 275 | Tagger: Signature{ 276 | Name: t.Tagger.Name, 277 | Email: t.Tagger.Email, 278 | Time: t.Tagger.When, 279 | }, 280 | TargetHash: t.Target.String(), 281 | } 282 | return 283 | } 284 | 285 | func DecodeCommit(content []byte) (res *Commit, err error) { 286 | obj := &plumbing.MemoryObject{} 287 | obj.SetType(plumbing.CommitObject) 288 | obj.Write(content) 289 | c := &object.Commit{} 290 | err = c.Decode(obj) 291 | if nil != err { 292 | return 293 | } 294 | res = &Commit{ 295 | Author: Signature{ 296 | Name: c.Author.Name, 297 | Email: c.Author.Email, 298 | Time: c.Author.When, 299 | }, 300 | Committer: Signature{ 301 | Name: c.Committer.Name, 302 | Email: c.Committer.Email, 303 | Time: c.Committer.When, 304 | }, 305 | TreeHash: c.TreeHash.String(), 306 | } 307 | return 308 | } 309 | 310 | func DecodeTree(content []byte) (res []*TreeEntry, err error) { 311 | obj := &plumbing.MemoryObject{} 312 | obj.SetType(plumbing.TreeObject) 313 | obj.Write(content) 314 | t := &object.Tree{} 315 | err = t.Decode(obj) 316 | if nil != err { 317 | return 318 | } 319 | res = make([]*TreeEntry, len(t.Entries)) 320 | for i, e := range t.Entries { 321 | res[i] = &TreeEntry{ 322 | Name: e.Name, 323 | Mode: uint32(e.Mode), 324 | Hash: e.Hash.String(), 325 | } 326 | } 327 | return 328 | } 329 | 330 | func trace(vals ...interface{}) func(vals ...interface{}) { 331 | return libtrace.Trace(1, "", vals...) 332 | } 333 | -------------------------------------------------------------------------------- /src/git/git_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * git_test.go 3 | * 4 | * Copyright 2021-2022 Bill Zissimopoulos 5 | */ 6 | /* 7 | * This file is part of Hubfs. 8 | * 9 | * You can redistribute it and/or modify it under the terms of the GNU 10 | * Affero General Public License version 3 as published by the Free 11 | * Software Foundation. 12 | */ 13 | 14 | package git 15 | 16 | import ( 17 | "os" 18 | "testing" 19 | 20 | "github.com/billziss-gh/golib/keyring" 21 | libtrace "github.com/billziss-gh/golib/trace" 22 | ) 23 | 24 | const remote = "https://github.com/winfsp/hubfs" 25 | const refName = "refs/heads/master" 26 | 27 | const hash0 = "90f898ae1f8d3c976f9224d92e3b08d7813e961e" 28 | const hash1 = "609d3b892764952ef69676e653e06b2ca904be18" 29 | const hash2 = "9b3aeb6b08911ee09ecc31c8c87e4905cf8b4dac" 30 | 31 | var token string 32 | 33 | func TestGetRefs(t *testing.T) { 34 | repository, err := OpenRepository(remote, token, "x-oauth-basic") 35 | if nil != err { 36 | t.Error(err) 37 | } 38 | defer repository.Close() 39 | 40 | refs, err := repository.GetRefs() 41 | if nil != err { 42 | t.Error(err) 43 | } 44 | found := false 45 | for n := range refs { 46 | if n == refName { 47 | found = true 48 | break 49 | } 50 | } 51 | if !found { 52 | t.Error() 53 | } 54 | 55 | refs, err = repository.GetRefs() 56 | if nil != err { 57 | t.Error(err) 58 | } 59 | found = false 60 | for n := range refs { 61 | if n == refName { 62 | found = true 63 | break 64 | } 65 | } 66 | if !found { 67 | t.Error() 68 | } 69 | } 70 | 71 | func TestFetchObjects(t *testing.T) { 72 | repository, err := OpenRepository(remote, token, "x-oauth-basic") 73 | if nil != err { 74 | t.Error(err) 75 | } 76 | defer repository.Close() 77 | 78 | wants := []string{ 79 | hash0, 80 | hash1, 81 | hash2, 82 | } 83 | found0 := false 84 | found1 := false 85 | found2 := false 86 | err = repository.FetchObjects(wants, 87 | func(hash string, ot ObjectType, content []byte) error { 88 | if hash0 == hash { 89 | found0 = true 90 | _, err := DecodeCommit(content) 91 | if nil != err { 92 | return err 93 | } 94 | } 95 | if hash1 == hash { 96 | found1 = true 97 | _, err := DecodeTree(content) 98 | if nil != err { 99 | return err 100 | } 101 | } 102 | if hash2 == hash { 103 | found2 = true 104 | _, err := DecodeTag(content) 105 | if nil != err { 106 | return err 107 | } 108 | } 109 | return nil 110 | }) 111 | if nil != err { 112 | t.Error(err) 113 | } 114 | if !found0 || !found1 || !found2 { 115 | t.Error() 116 | } 117 | 118 | wants = []string{ 119 | hash0, 120 | } 121 | found0 = false 122 | err = repository.FetchObjects(wants, 123 | func(hash string, ot ObjectType, content []byte) error { 124 | if hash0 == hash { 125 | found0 = true 126 | _, err := DecodeCommit(content) 127 | if nil != err { 128 | return err 129 | } 130 | } 131 | return nil 132 | }) 133 | if nil != err { 134 | t.Error(err) 135 | } 136 | if !found0 { 137 | t.Error() 138 | } 139 | 140 | wants = []string{ 141 | hash1, 142 | } 143 | found1 = false 144 | err = repository.FetchObjects(wants, 145 | func(hash string, ot ObjectType, content []byte) error { 146 | if hash1 == hash { 147 | found1 = true 148 | _, err := DecodeTree(content) 149 | if nil != err { 150 | return err 151 | } 152 | } 153 | return nil 154 | }) 155 | if nil != err { 156 | t.Error(err) 157 | } 158 | if !found1 { 159 | t.Error() 160 | } 161 | } 162 | 163 | func TestMain(m *testing.M) { 164 | libtrace.Verbose = true 165 | libtrace.Pattern = "github.com/winfsp/hubfs/*" 166 | 167 | var err error 168 | token, err = keyring.Get("hubfs", "github.com") 169 | if nil != err { 170 | token = "" 171 | } 172 | if "" == token { 173 | token = os.Getenv("HUBFS_TOKEN") 174 | } 175 | 176 | os.Exit(m.Run()) 177 | } 178 | -------------------------------------------------------------------------------- /src/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/winfsp/hubfs 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/billziss-gh/golib v0.2.0 7 | github.com/cli/browser v1.0.0 8 | github.com/cli/oauth v0.9.0 9 | github.com/go-git/go-git/v5 v5.2.0 10 | github.com/winfsp/cgofuse v1.5.1-0.20220421173602-ce7e5a65cac7 11 | ) 12 | 13 | replace github.com/go-git/go-git/v5 v5.2.0 => github.com/billziss-gh/go-git/v5 v5.2.1-0.20210325075736-c1624bffeb12 14 | -------------------------------------------------------------------------------- /src/go.sum: -------------------------------------------------------------------------------- 1 | github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs= 2 | github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= 3 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= 4 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= 5 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 6 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 7 | github.com/billziss-gh/go-git/v5 v5.2.1-0.20210325075736-c1624bffeb12 h1:7GkvcxnD8fdlVMiZGW4BKbIIUQ0/l8Vch8rYog5C5F0= 8 | github.com/billziss-gh/go-git/v5 v5.2.1-0.20210325075736-c1624bffeb12/go.mod h1:kh02eMX+wdqqxgNMEyq8YgwlIOsDOa9homkUq1PoTMs= 9 | github.com/billziss-gh/golib v0.2.0 h1:NyvcAQdfvM8xokKkKotiligKjKXzuQD4PPykg1nKc/8= 10 | github.com/billziss-gh/golib v0.2.0/go.mod h1:mZpUYANXZkDKSnyYbX9gfnyxwe0ddRhUtfXcsD5r8dw= 11 | github.com/cli/browser v1.0.0 h1:RIleZgXrhdiCVgFBSjtWwkLPUCWyhhhN5k5HGSBt1js= 12 | github.com/cli/browser v1.0.0/go.mod h1:IEWkHYbLjkhtjwwWlwTHW2lGxeS5gezEQBMLTwDHf5Q= 13 | github.com/cli/oauth v0.9.0 h1:nxBC0Df4tUzMkqffAB+uZvisOwT3/N9FpkfdTDtafxc= 14 | github.com/cli/oauth v0.9.0/go.mod h1:qd/FX8ZBD6n1sVNQO3aIdRxeu5LGw9WhKnYhIIoC2A4= 15 | github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI= 16 | github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= 17 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 18 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 19 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 20 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 21 | github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= 22 | github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= 23 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= 24 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= 25 | github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= 26 | github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= 27 | github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= 28 | github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= 29 | github.com/go-git/go-billy/v5 v5.0.0 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agRrHM= 30 | github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= 31 | github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12 h1:PbKy9zOy4aAKrJ5pibIRpVO2BXnK1Tlcg+caKI7Ox5M= 32 | github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw= 33 | github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= 34 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 35 | github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg= 36 | github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 37 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= 38 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 39 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 40 | github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY= 41 | github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 42 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 43 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 44 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 45 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 46 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 47 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 48 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 49 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 50 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 51 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 52 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 53 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 54 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 55 | github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= 56 | github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 57 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 58 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 59 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 60 | github.com/winfsp/cgofuse v1.5.1-0.20220421173602-ce7e5a65cac7 h1:Lvi511+8ZWFr9YWbvXkxudGSjcrjM1NVPDIPeWusvDk= 61 | github.com/winfsp/cgofuse v1.5.1-0.20220421173602-ce7e5a65cac7/go.mod h1:uxjoF2jEYT3+x+vC2KJddEGdk/LU8pRowXmyVMHSV5I= 62 | github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70= 63 | github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= 64 | golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 65 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 66 | golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM= 67 | golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 68 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 69 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0= 70 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 71 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 72 | golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 73 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 74 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So= 75 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 76 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 77 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 78 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 79 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 80 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 81 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 82 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 83 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 84 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= 85 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 86 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 87 | gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= 88 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 89 | -------------------------------------------------------------------------------- /src/httputil/httputil.go: -------------------------------------------------------------------------------- 1 | /* 2 | * httputil.go 3 | * 4 | * Copyright 2021-2022 Bill Zissimopoulos 5 | */ 6 | /* 7 | * This file is part of Hubfs. 8 | * 9 | * You can redistribute it and/or modify it under the terms of the GNU 10 | * Affero General Public License version 3 as published by the Free 11 | * Software Foundation. 12 | */ 13 | 14 | package httputil 15 | 16 | import ( 17 | "crypto/tls" 18 | "net/http" 19 | "time" 20 | 21 | "github.com/billziss-gh/golib/retry" 22 | ) 23 | 24 | var ( 25 | DefaultRetryCount = 10 26 | DefaultSleep = time.Second 27 | DefaultMaxSleep = time.Second * 30 28 | DefaultClient *http.Client 29 | DefaultTransport *http.Transport 30 | ) 31 | 32 | func init() { 33 | DefaultTransport = http.DefaultTransport.(*http.Transport).Clone() 34 | if nil == DefaultTransport.TLSClientConfig { 35 | DefaultTransport.TLSClientConfig = &tls.Config{} 36 | } 37 | DefaultClient = &http.Client{ 38 | Transport: &transport{ 39 | RoundTripper: DefaultTransport, 40 | }, 41 | } 42 | } 43 | 44 | type transport struct { 45 | http.RoundTripper 46 | } 47 | 48 | func (t *transport) RoundTrip(req *http.Request) (rsp *http.Response, err error) { 49 | retry.Retry( 50 | retry.Count(DefaultRetryCount), 51 | retry.Backoff(DefaultSleep, DefaultMaxSleep), 52 | func(i int) bool { 53 | 54 | rsp, err = t.RoundTripper.RoundTrip(req) 55 | 56 | // retry on connection errors without body 57 | if nil != err { 58 | return nil == req.Body 59 | } 60 | 61 | // retry on HTTP 429, 503, 509 62 | switch rsp.StatusCode { 63 | case 429, 503, 509: 64 | rsp.Body.Close() 65 | return true 66 | } 67 | 68 | return false 69 | }) 70 | 71 | return 72 | } 73 | -------------------------------------------------------------------------------- /src/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | * main.go 3 | * 4 | * Copyright 2021-2022 Bill Zissimopoulos 5 | */ 6 | /* 7 | * This file is part of Hubfs. 8 | * 9 | * You can redistribute it and/or modify it under the terms of the GNU 10 | * Affero General Public License version 3 as published by the Free 11 | * Software Foundation. 12 | */ 13 | 14 | package main 15 | 16 | import ( 17 | "errors" 18 | "flag" 19 | "fmt" 20 | "net/url" 21 | "os" 22 | "os/exec" 23 | "os/user" 24 | "path/filepath" 25 | "runtime" 26 | "strings" 27 | 28 | "github.com/billziss-gh/golib/keyring" 29 | libtrace "github.com/billziss-gh/golib/trace" 30 | "github.com/winfsp/cgofuse/fuse" 31 | "github.com/winfsp/hubfs/fs/hubfs" 32 | "github.com/winfsp/hubfs/fs/port" 33 | "github.com/winfsp/hubfs/prov" 34 | "github.com/winfsp/hubfs/util" 35 | ) 36 | 37 | var ( 38 | MyProductName = "HUBFS" 39 | MyDescription = "File system for GitHub" 40 | MyCopyright = "2021-2022 Bill Zissimopoulos" 41 | MyVersion = "DEVVER" 42 | MyProductVersion = "PRDVER" 43 | MyProductTag = "" 44 | ) 45 | 46 | var progname = strings.TrimSuffix(filepath.Base(os.Args[0]), ".exe") 47 | 48 | func warn(format string, a ...interface{}) { 49 | format = "%s: " + format + "\n" 50 | a = append([]interface{}{progname}, a...) 51 | fmt.Fprintf(os.Stderr, format, a...) 52 | } 53 | 54 | func newClientWithKey(provider prov.Provider, authkey string) ( 55 | client prov.Client, err error) { 56 | token, err := keyring.Get(MyProductName, authkey) 57 | if nil == err { 58 | client, err = provider.NewClient(token) 59 | if nil != err { 60 | keyring.Delete(MyProductName, authkey) 61 | } 62 | } 63 | return 64 | } 65 | 66 | func oauthNewClientWithKey(provider prov.Provider, authkey string) ( 67 | client prov.Client, err error) { 68 | token, err := provider.Auth() 69 | if nil == err { 70 | client, err = provider.NewClient(token) 71 | if nil == err { 72 | keyring.Set(MyProductName, authkey, token) 73 | } 74 | } 75 | return 76 | } 77 | 78 | func gitauthNewClientWithUri(provider prov.Provider, uri *url.URL) ( 79 | client prov.Client, err error) { 80 | cmd := exec.Command("git", "credential", "fill") 81 | cmd.Stdin = strings.NewReader(fmt.Sprintf("protocol=https\nhost=%s\n", uri.Host)) 82 | out, err := cmd.Output() 83 | if nil == err { 84 | token := "" 85 | for _, line := range strings.Split(string(out), "\n") { 86 | t := strings.TrimPrefix(line, "password=") 87 | if line != t { 88 | token = t 89 | break 90 | } 91 | } 92 | if "" == token { 93 | return nil, errors.New("gitauth: no password") 94 | } 95 | client, err = provider.NewClient(token) 96 | } 97 | return 98 | } 99 | 100 | func mount(client prov.Client, overlay bool, prefix string, mntpnt string, config []string) bool { 101 | mntopt := []string{} 102 | for _, s := range config { 103 | mntopt = append(mntopt, "-o"+s) 104 | } 105 | 106 | caseins := false 107 | if "windows" == runtime.GOOS || "darwin" == runtime.GOOS { 108 | caseins = true 109 | } 110 | 111 | if caseins { 112 | client.SetConfig([]string{"config._caseins=1"}) 113 | } else { 114 | client.SetConfig([]string{"config._caseins=0"}) 115 | } 116 | client.StartExpiration() 117 | defer client.StopExpiration() 118 | 119 | fs := hubfs.New(hubfs.Config{ 120 | Client: client, 121 | Prefix: prefix, 122 | Caseins: caseins, 123 | Overlay: overlay, 124 | }) 125 | host := fuse.NewFileSystemHost(fs) 126 | host.SetCapCaseInsensitive(caseins) 127 | host.SetCapReaddirPlus(true) 128 | return host.Mount(mntpnt, mntopt) 129 | } 130 | 131 | func run() int { 132 | default_mntopt := util.Optlist{} 133 | switch runtime.GOOS { 134 | case "windows": 135 | default_mntopt = util.Optlist{"uid=-1", "gid=-1", "rellinks", "FileInfoTimeout=-1"} 136 | case "linux": 137 | default_mntopt = util.Optlist{"uid=-1", "gid=-1", "default_permissions"} 138 | case "darwin": 139 | default_mntopt = util.Optlist{"uid=-1", "gid=-1", "default_permissions", "noapplexattr"} 140 | } 141 | 142 | debug := false 143 | printver := false 144 | authmeth := "full" 145 | authkey := "" 146 | authonly := false 147 | readonly := false 148 | fullrefs := false 149 | filter := util.Optlist{} 150 | mntopt := util.Optlist{} 151 | remote := "github.com" 152 | mntpnt := "" 153 | config := []string{"config.dir=:"} 154 | 155 | flag.Usage = func() { 156 | fmt.Fprintf(os.Stderr, "usage: %s [options] [remote] mountpoint\n\n", progname) 157 | flag.PrintDefaults() 158 | fmt.Fprintf(os.Stderr, "\nremotes:\n") 159 | for _, n := range prov.GetProviderClassNames() { 160 | fmt.Fprintf(os.Stderr, " %s\n", prov.GetProviderClassHelp(n)) 161 | } 162 | } 163 | 164 | flag.BoolVar(&debug, "d", debug, "debug output") 165 | flag.BoolVar(&printver, "version", printver, "print version information") 166 | flag.StringVar(&authmeth, "auth", "", 167 | "`method` is from list below; auth tokens are stored in system keyring\n"+ 168 | "- force perform interactive auth even if token present\n"+ 169 | "- full perform interactive auth if token not present (default)\n"+ 170 | "- required auth token required to be present\n"+ 171 | "- optional auth token will be used if present\n"+ 172 | "- none do not use auth token even if present\n"+ 173 | "- git use `git credential` for auth; do not use system keyring\n"+ 174 | "- token=T use specified auth token T; do not use system keyring") 175 | flag.StringVar(&authkey, "authkey", authkey, "`name` of key that stores auth token in system keyring") 176 | flag.BoolVar(&authonly, "authonly", authonly, "perform auth only; do not mount") 177 | flag.BoolVar(&readonly, "readonly", readonly, "read only file system") 178 | flag.BoolVar(&fullrefs, "fullrefs", fullrefs, "full format refs (refs+heads+master instead of master)") 179 | flag.Var(&filter, "filter", 180 | "list of `rules` that determine repo availability\n"+ 181 | "- list form: rule1,rule2,...\n"+ 182 | "- rule form: [+-]owner or [+-]owner/repo\n"+ 183 | "- rule is include (+) or exclude (-) (default: include)\n"+ 184 | "- rule owner/repo can use wildcards for pattern matching") 185 | flag.Var(&mntopt, "o", "FUSE mount `options`\n(default: "+strings.Join(default_mntopt, ",")+")") 186 | 187 | util.InvokeEvent("main.Flagvar", nil) 188 | 189 | flag.Parse() 190 | 191 | if printver { 192 | name := MyProductName 193 | if "" != MyProductTag { 194 | name += " " + MyProductTag 195 | } 196 | fmt.Printf("%s %s (%s) - %s\nCopyright %s\n\n", 197 | name, MyProductVersion, MyVersion, MyDescription, MyCopyright) 198 | util.InvokeEvent("main.Printver", nil) 199 | fmt.Printf("Providers:\n") 200 | for _, n := range prov.GetProviderClassNames() { 201 | fmt.Printf(" %s\n", n) 202 | } 203 | return 0 204 | } 205 | 206 | switch flag.NArg() { 207 | case 1: 208 | mntpnt = flag.Arg(0) 209 | case 2: 210 | remote = flag.Arg(0) 211 | mntpnt = flag.Arg(1) 212 | default: 213 | if !authonly { 214 | flag.Usage() 215 | return 2 216 | } 217 | } 218 | switch authmeth { 219 | case "": 220 | authmeth = "full" 221 | case "force", "full", "required", "optional", "git": 222 | case "none": 223 | if authonly { 224 | flag.Usage() 225 | return 2 226 | } 227 | default: 228 | if strings.HasPrefix(authmeth, "token=") { 229 | break 230 | } 231 | flag.Usage() 232 | return 2 233 | } 234 | 235 | if debug { 236 | libtrace.Verbose = true 237 | libtrace.Pattern = "*,github.com/winfsp/hubfs/*,github.com/winfsp/hubfs/fs/*" 238 | } 239 | 240 | util.InvokeEvent("main.Flagrun", nil) 241 | 242 | uri, err := url.Parse(remote) 243 | if nil != uri && "" == uri.Scheme { 244 | uri, err = url.Parse("https://" + remote) 245 | } 246 | if nil != err { 247 | warn("invalid remote: %s", remote) 248 | return 1 249 | } 250 | 251 | provider := prov.NewProviderInstance(uri) 252 | if nil == provider { 253 | warn("unknown provider: %s", prov.GetProviderInstanceName(uri)) 254 | return 1 255 | } 256 | 257 | if "" == authkey { 258 | authkey = prov.GetProviderInstanceName(uri) 259 | } 260 | 261 | var client prov.Client 262 | switch authmeth { 263 | case "force": 264 | client, err = oauthNewClientWithKey(provider, authkey) 265 | case "full": 266 | client, err = newClientWithKey(provider, authkey) 267 | if nil != err { 268 | client, err = oauthNewClientWithKey(provider, authkey) 269 | } 270 | case "required": 271 | client, err = newClientWithKey(provider, authkey) 272 | case "optional": 273 | client, err = newClientWithKey(provider, authkey) 274 | if nil != err { 275 | client, err = provider.NewClient("") 276 | } 277 | case "none": 278 | client, err = provider.NewClient("") 279 | case "git": 280 | client, err = gitauthNewClientWithUri(provider, uri) 281 | default: 282 | if strings.HasPrefix(authmeth, "token=") { 283 | client, err = provider.NewClient(strings.TrimPrefix(authmeth, "token=")) 284 | } 285 | } 286 | if nil != err { 287 | warn("client error: %v", err) 288 | return 1 289 | } 290 | 291 | if !authonly { 292 | if 0 == len(mntopt) { 293 | mntopt = default_mntopt 294 | } 295 | fmt.Printf("%s -o %s %s %s\n", progname, strings.Join(mntopt, ","), remote, mntpnt) 296 | 297 | if debug { 298 | mntopt = append(mntopt, "debug") 299 | } 300 | 301 | for _, m := range mntopt { 302 | for _, s := range strings.Split(m, ",") { 303 | if "windows" != runtime.GOOS { 304 | /* on Windows, WinFsp handles uid=-1,gid=-1 for us */ 305 | if "uid=-1" == s { 306 | u, _ := user.Current() 307 | s = "uid=" + u.Uid 308 | } else if "gid=-1" == s { 309 | u, _ := user.Current() 310 | s = "gid=" + u.Gid 311 | } 312 | } 313 | config = append(config, s) 314 | } 315 | } 316 | 317 | if fullrefs { 318 | config = append(config, "config._fullrefs=1") 319 | } 320 | 321 | for _, f := range filter { 322 | for _, s := range strings.Split(f, ",") { 323 | config = append(config, "config._filter="+s) 324 | } 325 | } 326 | 327 | config, err = client.SetConfig(config) 328 | if nil != err { 329 | warn("config error: %v", err) 330 | return 1 331 | } 332 | 333 | port.Umask(0) 334 | 335 | if !mount(client, !readonly, uri.Path, mntpnt, config) { 336 | return 1 337 | } 338 | } 339 | 340 | return 0 341 | } 342 | 343 | func main() { 344 | ec := run() 345 | os.Exit(ec) 346 | } 347 | -------------------------------------------------------------------------------- /src/prov/cache.go: -------------------------------------------------------------------------------- 1 | /* 2 | * cache.go 3 | * 4 | * Copyright 2021-2022 Bill Zissimopoulos 5 | */ 6 | /* 7 | * This file is part of Hubfs. 8 | * 9 | * You can redistribute it and/or modify it under the terms of the GNU 10 | * Affero General Public License version 3 as published by the Free 11 | * Software Foundation. 12 | */ 13 | 14 | package prov 15 | 16 | import ( 17 | "strings" 18 | "sync" 19 | "time" 20 | 21 | libcache "github.com/billziss-gh/golib/cache" 22 | ) 23 | 24 | type cacheImap struct { 25 | libcache.Map 26 | } 27 | 28 | func NewCacheImap(list *libcache.MapItem) *cacheImap { 29 | m := &cacheImap{} 30 | m.Map.InitMap(list) 31 | return m 32 | } 33 | 34 | func (m *cacheImap) Items() map[string]*libcache.MapItem { 35 | return m.Map.Items() 36 | } 37 | 38 | func (m *cacheImap) Get(key string) (*libcache.MapItem, bool) { 39 | return m.Map.Get(strings.ToUpper(key)) 40 | } 41 | 42 | func (m *cacheImap) Set(key string, newitem *libcache.MapItem, expirable bool) { 43 | m.Map.Set(strings.ToUpper(key), newitem, expirable) 44 | } 45 | 46 | func (m *cacheImap) Delete(key string) { 47 | m.Map.Delete(strings.ToUpper(key)) 48 | } 49 | 50 | type cache struct { 51 | Value interface{} 52 | lock sync.Locker 53 | lrulist libcache.MapItem 54 | ttl time.Duration 55 | stopC chan bool 56 | stopW *sync.WaitGroup 57 | } 58 | 59 | type cacheItem struct { 60 | libcache.MapItem 61 | lastUsedTime time.Time 62 | inUse int64 63 | } 64 | 65 | type expirable interface { 66 | expire(c *cache, currentTime time.Time) bool 67 | } 68 | 69 | func newCache(lock sync.Locker) *cache { 70 | c := &cache{} 71 | c.lock = lock 72 | c.lrulist.Empty() 73 | return c 74 | } 75 | 76 | func (c *cache) newCacheMap() *libcache.Map { 77 | return libcache.NewMap(&c.lrulist) 78 | } 79 | 80 | func (c *cache) newCacheImap() *cacheImap { 81 | return NewCacheImap(&c.lrulist) 82 | } 83 | 84 | func (c *cache) touchCacheItem(citem *cacheItem, delta int) { 85 | citem.lastUsedTime = time.Now().Add(c.ttl) 86 | citem.inUse += int64(delta) 87 | } 88 | 89 | func (c *cache) expireCacheItem(citem *cacheItem, currentTime time.Time, fn func()) bool { 90 | if citem.lastUsedTime.After(currentTime) { 91 | return false 92 | } 93 | citem.lastUsedTime = currentTime.Add(c.ttl) 94 | citem.Remove() 95 | citem.InsertTail(&c.lrulist) 96 | if 0 >= citem.inUse { 97 | fn() 98 | } 99 | return true 100 | } 101 | 102 | func (c *cache) startExpiration(timeToLive time.Duration) { 103 | c.ttl = timeToLive 104 | c.stopC = make(chan bool, 1) 105 | c.stopW = &sync.WaitGroup{} 106 | c.stopW.Add(1) 107 | go c._tick() 108 | } 109 | 110 | func (c *cache) stopExpiration() { 111 | c.stopC <- true 112 | c.stopW.Wait() 113 | close(c.stopC) 114 | c.ttl = 0 115 | c.stopC = nil 116 | c.stopW = nil 117 | } 118 | 119 | func (c *cache) _tick() { 120 | defer c.stopW.Done() 121 | ticker := time.NewTicker(1 * time.Second) 122 | for { 123 | select { 124 | case <-ticker.C: 125 | currentTime := time.Now() 126 | c.lock.Lock() 127 | c.lrulist.Expire(func(l, item *libcache.MapItem) bool { 128 | return item.Value.(expirable).expire(c, currentTime) 129 | }) 130 | c.lock.Unlock() 131 | case <-c.stopC: 132 | ticker.Stop() 133 | return 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/prov/client.go: -------------------------------------------------------------------------------- 1 | /* 2 | * client.go 3 | * 4 | * Copyright 2021-2022 Bill Zissimopoulos 5 | */ 6 | /* 7 | * This file is part of Hubfs. 8 | * 9 | * You can redistribute it and/or modify it under the terms of the GNU 10 | * Affero General Public License version 3 as published by the Free 11 | * Software Foundation. 12 | */ 13 | 14 | package prov 15 | 16 | import ( 17 | "os" 18 | "path/filepath" 19 | "strings" 20 | "sync" 21 | "time" 22 | 23 | "github.com/billziss-gh/golib/appdata" 24 | ) 25 | 26 | type client struct { 27 | api clientApi 28 | dir string 29 | keepdir bool 30 | caseins bool 31 | fullrefs bool 32 | ttl time.Duration 33 | lock sync.Mutex 34 | cache *cache 35 | owners *cacheImap 36 | filter *filterType 37 | } 38 | 39 | type owner struct { 40 | cacheItem 41 | repositories *cacheImap 42 | FName string 43 | FKind string 44 | } 45 | 46 | type repository struct { 47 | cacheItem 48 | Repository 49 | keepdir bool 50 | FName string 51 | FRemote string 52 | } 53 | 54 | type clientApi interface { 55 | getIdent() string 56 | getGitCredentials() (string, string) 57 | getOwner(owner string) (res *owner, err error) 58 | getRepositories(owner string, kind string) (res []*repository, err error) 59 | } 60 | 61 | func (c *client) init(api clientApi) { 62 | c.api = api 63 | c.cache = newCache(&c.lock) 64 | c.cache.Value = c 65 | } 66 | 67 | func configValue(s string, k string, v *string) bool { 68 | if len(s) >= len(k) && s[:len(k)] == k { 69 | *v = s[len(k):] 70 | return true 71 | } 72 | return false 73 | } 74 | 75 | func (c *client) SetConfig(config []string) ([]string, error) { 76 | res := []string{} 77 | for _, s := range config { 78 | v := "" 79 | switch { 80 | case configValue(s, "config.dir=", &v): 81 | if ":" == v { 82 | if d, e := appdata.CacheDir(); nil == e { 83 | if p, e := os.Executable(); nil == e { 84 | n := strings.TrimSuffix(filepath.Base(p), ".exe") 85 | v = filepath.Join(d, n, c.api.getIdent()) 86 | c.dir = v 87 | c.keepdir = false 88 | } 89 | } 90 | } else { 91 | c.dir = v 92 | c.keepdir = true 93 | } 94 | case configValue(s, "config.ttl=", &v): 95 | if ttl, e := time.ParseDuration(v); nil == e && 0 < ttl { 96 | c.ttl = ttl 97 | } 98 | case configValue(s, "config._caseins=", &v): 99 | if "1" == v { 100 | c.caseins = true 101 | } else { 102 | c.caseins = false 103 | } 104 | case configValue(s, "config._fullrefs=", &v): 105 | if "1" == v { 106 | c.fullrefs = true 107 | } else { 108 | c.fullrefs = false 109 | } 110 | case configValue(s, "config._filter=", &v): 111 | if nil == c.filter { 112 | c.filter = &filterType{} 113 | } 114 | c.filter.addRule(v) 115 | default: 116 | res = append(res, s) 117 | } 118 | } 119 | 120 | return res, nil 121 | } 122 | 123 | func (c *client) GetDirectory() string { 124 | c.lock.Lock() 125 | dir := c.dir 126 | c.lock.Unlock() 127 | return dir 128 | } 129 | 130 | func (c *client) GetOwners() ([]Owner, error) { 131 | return []Owner{}, nil 132 | } 133 | 134 | func (c *client) OpenOwner(name string) (Owner, error) { 135 | var res *owner 136 | var err error 137 | 138 | if nil != c.filter && !c.filter.match(name) { 139 | return nil, ErrNotFound 140 | } 141 | 142 | c.lock.Lock() 143 | if nil != c.owners { 144 | item, ok := c.owners.Get(name) 145 | if ok { 146 | res = item.Value.(*owner) 147 | c.cache.touchCacheItem(&res.cacheItem, +1) 148 | c.lock.Unlock() 149 | return res, nil 150 | } 151 | } 152 | c.lock.Unlock() 153 | 154 | res, err = c.api.getOwner(name) 155 | if nil != err { 156 | return nil, err 157 | } 158 | 159 | c.lock.Lock() 160 | if nil == c.owners { 161 | c.owners = c.cache.newCacheImap() 162 | } 163 | item, ok := c.owners.Get(name) 164 | if ok { 165 | res = item.Value.(*owner) 166 | } else { 167 | c.owners.Set(name, &res.MapItem, true) 168 | } 169 | c.cache.touchCacheItem(&res.cacheItem, +1) 170 | c.lock.Unlock() 171 | return res, nil 172 | } 173 | 174 | func (c *client) CloseOwner(O Owner) { 175 | c.lock.Lock() 176 | c.cache.touchCacheItem(&O.(*owner).cacheItem, -1) 177 | c.lock.Unlock() 178 | } 179 | 180 | func (c *client) ensureRepositories(o *owner, fn func() error) error { 181 | c.lock.Lock() 182 | if nil != o.repositories { 183 | err := fn() 184 | c.lock.Unlock() 185 | return err 186 | } 187 | c.lock.Unlock() 188 | 189 | repositories, err := c.api.getRepositories(o.FName, o.FKind) 190 | if nil != err { 191 | return err 192 | } 193 | 194 | c.lock.Lock() 195 | if nil == o.repositories { 196 | o.repositories = c.cache.newCacheImap() 197 | for _, elm := range repositories { 198 | if nil != c.filter && !c.filter.match(o.FName+"/"+elm.FName) { 199 | continue 200 | } 201 | o.repositories.Set(elm.FName, &elm.MapItem, true) 202 | c.cache.touchCacheItem(&elm.cacheItem, 0) 203 | } 204 | } 205 | err = fn() 206 | c.lock.Unlock() 207 | return err 208 | } 209 | 210 | func (c *client) GetRepositories(O Owner) ([]Repository, error) { 211 | var res []Repository 212 | var err error 213 | 214 | o := O.(*owner) 215 | err = c.ensureRepositories(o, func() error { 216 | res = make([]Repository, len(o.repositories.Items())) 217 | i := 0 218 | for _, elm := range o.repositories.Items() { 219 | res[i] = elm.Value.(Repository) 220 | i++ 221 | } 222 | return nil 223 | }) 224 | 225 | return res, err 226 | } 227 | 228 | func (c *client) OpenRepository(O Owner, name string) (Repository, error) { 229 | var res *repository 230 | var err error 231 | 232 | o := O.(*owner) 233 | err = c.ensureRepositories(o, func() error { 234 | item, ok := o.repositories.Get(name) 235 | if !ok { 236 | return ErrNotFound 237 | } 238 | res = item.Value.(*repository) 239 | if emptyRepository == res.Repository { 240 | u, p := c.api.getGitCredentials() 241 | r := newGitRepository(res.FRemote, u, p, c.caseins, c.fullrefs) 242 | if "" != c.dir { 243 | err = r.SetDirectory(filepath.Join(c.dir, o.FName, res.FName)) 244 | if nil != err { 245 | return err 246 | } 247 | } 248 | res.Repository = r 249 | } 250 | c.cache.touchCacheItem(&res.cacheItem, +1) 251 | return nil 252 | }) 253 | if nil != err { 254 | return nil, err 255 | } 256 | 257 | return res, nil 258 | } 259 | 260 | func (c *client) CloseRepository(R Repository) { 261 | c.lock.Lock() 262 | c.cache.touchCacheItem(&R.(*repository).cacheItem, -1) 263 | c.lock.Unlock() 264 | } 265 | 266 | func (c *client) StartExpiration() { 267 | ttl := 30 * time.Second 268 | if 0 != c.ttl { 269 | ttl = c.ttl 270 | } 271 | c.cache.startExpiration(ttl) 272 | } 273 | 274 | func (c *client) StopExpiration() { 275 | c.cache.stopExpiration() 276 | 277 | c.lock.Lock() 278 | if "" == c.dir || c.keepdir { 279 | c.lock.Unlock() 280 | return 281 | } 282 | tmpdir := c.dir + time.Now().Format(".20060102T150405.000Z") 283 | err := os.Rename(c.dir, tmpdir) 284 | c.lock.Unlock() 285 | if nil == err { 286 | os.RemoveAll(tmpdir) 287 | } 288 | } 289 | 290 | func (o *owner) Name() string { 291 | return o.FName 292 | } 293 | 294 | func (o *owner) expire(c *cache, currentTime time.Time) bool { 295 | return c.expireCacheItem(&o.cacheItem, currentTime, func() { 296 | if nil != o.repositories { 297 | for _, elm := range o.repositories.Items() { 298 | r := elm.Value.(*repository) 299 | if emptyRepository != r.Repository { 300 | // do not expire Owner that has unexpired repositories 301 | return 302 | } 303 | } 304 | } 305 | 306 | c := c.Value.(*client) 307 | c.owners.Delete(o.FName) 308 | tracef("%s", o.FName) 309 | }) 310 | } 311 | 312 | func (r *repository) Name() string { 313 | return r.FName 314 | } 315 | 316 | func (r *repository) keep() bool { 317 | var list []string 318 | if dir := r.GetDirectory(); "" != dir { 319 | list, _ = filepath.Glob(filepath.Join(dir, "files/*/.keep")) 320 | } 321 | return 0 != len(list) 322 | } 323 | 324 | func (r *repository) expire(c *cache, currentTime time.Time) bool { 325 | return c.expireCacheItem(&r.cacheItem, currentTime, func() { 326 | if emptyRepository == r.Repository { 327 | return 328 | } 329 | 330 | if r.keepdir || r.keep() { 331 | tracef("repo=%#v", r.FRemote) 332 | } else { 333 | err := r.RemoveDirectory() 334 | tracef("repo=%#v [RemoveDirectory() = %v]", r.FRemote, err) 335 | } 336 | r.Close() 337 | r.Repository = emptyRepository 338 | }) 339 | } 340 | 341 | var _ Client = (*client)(nil) 342 | var _ Owner = (*owner)(nil) 343 | var _ Repository = (*repository)(nil) 344 | -------------------------------------------------------------------------------- /src/prov/emptyrepo.go: -------------------------------------------------------------------------------- 1 | /* 2 | * emptyrepo.go 3 | * 4 | * Copyright 2021-2022 Bill Zissimopoulos 5 | */ 6 | /* 7 | * This file is part of Hubfs. 8 | * 9 | * You can redistribute it and/or modify it under the terms of the GNU 10 | * Affero General Public License version 3 as published by the Free 11 | * Software Foundation. 12 | */ 13 | 14 | package prov 15 | 16 | import ( 17 | "io" 18 | ) 19 | 20 | // When using: 21 | // 22 | // var emptyRepository Repository = &emptyRepositoryT{} 23 | // 24 | // The emptyRepository variable is not initialized (at least during testing). 25 | // May be related to https://github.com/golang/go/issues/44956 26 | var emptyRepository Repository 27 | 28 | type emptyRepositoryT struct { 29 | } 30 | 31 | func (*emptyRepositoryT) Close() (err error) { 32 | return nil 33 | } 34 | 35 | func (*emptyRepositoryT) GetDirectory() string { 36 | return "" 37 | } 38 | 39 | func (*emptyRepositoryT) SetDirectory(path string) error { 40 | return nil 41 | } 42 | 43 | func (*emptyRepositoryT) RemoveDirectory() error { 44 | return nil 45 | } 46 | 47 | func (*emptyRepositoryT) Name() string { 48 | return "" 49 | } 50 | 51 | func (*emptyRepositoryT) GetRefs() ([]Ref, error) { 52 | return []Ref{}, nil 53 | } 54 | 55 | func (*emptyRepositoryT) GetRef(name string) (Ref, error) { 56 | return nil, ErrNotFound 57 | } 58 | 59 | func (*emptyRepositoryT) GetTempRef(name string) (Ref, error) { 60 | return nil, ErrNotFound 61 | } 62 | 63 | func (*emptyRepositoryT) GetTree(ref Ref, entry TreeEntry) ([]TreeEntry, error) { 64 | return []TreeEntry{}, nil 65 | } 66 | 67 | func (*emptyRepositoryT) GetTreeEntry(ref Ref, entry TreeEntry, name string) (TreeEntry, error) { 68 | return nil, ErrNotFound 69 | } 70 | 71 | func (*emptyRepositoryT) GetBlobReader(entry TreeEntry) (io.ReaderAt, error) { 72 | return nil, ErrNotFound 73 | } 74 | 75 | func (*emptyRepositoryT) GetModule(ref Ref, path string, rootrel bool) (string, error) { 76 | return "", ErrNotFound 77 | } 78 | 79 | func init() { 80 | emptyRepository = &emptyRepositoryT{} 81 | } 82 | -------------------------------------------------------------------------------- /src/prov/filter.go: -------------------------------------------------------------------------------- 1 | /* 2 | * filter.go 3 | * 4 | * Copyright 2021-2022 Bill Zissimopoulos 5 | */ 6 | /* 7 | * This file is part of Hubfs. 8 | * 9 | * You can redistribute it and/or modify it under the terms of the GNU 10 | * Affero General Public License version 3 as published by the Free 11 | * Software Foundation. 12 | */ 13 | 14 | package prov 15 | 16 | import ( 17 | pathutil "path" 18 | "strings" 19 | ) 20 | 21 | type filterType [2][]string 22 | 23 | func (filter *filterType) addRule(rule string) { 24 | rule = strings.ToUpper(rule) 25 | sign := '+' 26 | patt := rule 27 | if strings.HasPrefix(rule, "+") { 28 | patt = rule[1:] 29 | } else if strings.HasPrefix(rule, "-") { 30 | sign = '-' 31 | patt = rule[1:] 32 | } 33 | patt = pathutil.Clean(patt) 34 | patt = strings.TrimPrefix(patt, "/") 35 | 36 | slashes := 0 37 | for i := 0; len(patt) > i; i++ { 38 | if '/' == patt[i] { 39 | slashes++ 40 | if 2 == slashes { 41 | patt = patt[:i] 42 | slashes-- 43 | break 44 | } 45 | } 46 | } 47 | 48 | switch slashes { 49 | case 0: 50 | filter[0] = append(filter[0], string(sign)+patt) 51 | filter[1] = append(filter[1], string(sign)+patt+"/*") 52 | case 1: 53 | if '+' == sign { 54 | filter[0] = append(filter[0], string(sign)+pathutil.Dir(patt)) 55 | } 56 | filter[1] = append(filter[1], string(sign)+patt) 57 | } 58 | } 59 | 60 | func (filter *filterType) match(path string) bool { 61 | slashes := 0 62 | for i := 0; len(path) > i; i++ { 63 | if '/' == path[i] { 64 | slashes++ 65 | if 2 == slashes { 66 | path = path[:i] 67 | slashes-- 68 | break 69 | } 70 | } 71 | } 72 | 73 | path = strings.ToUpper(path) 74 | res := false 75 | for _, rule := range filter[slashes] { 76 | sign := rule[0] 77 | patt := rule[1:] 78 | m, e := pathutil.Match(patt, path) 79 | if nil != e { 80 | return false 81 | } 82 | if m { 83 | if '+' == sign { 84 | res = res || m 85 | } else { 86 | res = res && !m 87 | } 88 | } 89 | } 90 | return res 91 | } 92 | -------------------------------------------------------------------------------- /src/prov/filter_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * filter_test.go 3 | * 4 | * Copyright 2021-2022 Bill Zissimopoulos 5 | */ 6 | /* 7 | * This file is part of Hubfs. 8 | * 9 | * You can redistribute it and/or modify it under the terms of the GNU 10 | * Affero General Public License version 3 as published by the Free 11 | * Software Foundation. 12 | */ 13 | 14 | package prov 15 | 16 | import ( 17 | "testing" 18 | ) 19 | 20 | func TestFilter(t *testing.T) { 21 | var filter filterType 22 | 23 | config := func(rules []string) { 24 | filter = filterType{} 25 | for _, rule := range rules { 26 | filter.addRule(rule) 27 | } 28 | } 29 | expect := func(path string, e bool) { 30 | m := filter.match(path) 31 | if e != m { 32 | t.Errorf("path %q expect %v got %v", path, e, m) 33 | } 34 | } 35 | 36 | config([]string{ 37 | "/*", 38 | }) 39 | expect("", true) 40 | expect("a", true) 41 | expect("b", true) 42 | expect("a/1", true) 43 | expect("a/2", true) 44 | expect("a/1/foo", true) 45 | 46 | config([]string{ 47 | "-*", 48 | }) 49 | expect("", false) 50 | expect("a", false) 51 | expect("b", false) 52 | expect("a/1", false) 53 | expect("a/2", false) 54 | expect("a/1/foo", false) 55 | 56 | config([]string{ 57 | "owner", 58 | }) 59 | expect("", false) 60 | expect("a", false) 61 | expect("owner", true) 62 | expect("a/1", false) 63 | expect("owner/1", true) 64 | 65 | config([]string{ 66 | "owner", 67 | "owner2", 68 | }) 69 | expect("", false) 70 | expect("a", false) 71 | expect("owner", true) 72 | expect("owner2", true) 73 | expect("a/1", false) 74 | expect("owner/1", true) 75 | expect("owner2/1", true) 76 | 77 | config([]string{ 78 | "-owner", 79 | "-owner2", 80 | }) 81 | expect("", false) 82 | expect("a", false) 83 | expect("owner", false) 84 | expect("owner2", false) 85 | expect("a/1", false) 86 | expect("owner/1", false) 87 | expect("owner2/1", false) 88 | 89 | config([]string{ 90 | "*", 91 | "-owner", 92 | "-owner2", 93 | }) 94 | expect("", true) 95 | expect("a", true) 96 | expect("owner", false) 97 | expect("owner2", false) 98 | expect("a/1", true) 99 | expect("owner/1", false) 100 | expect("owner2/1", false) 101 | 102 | config([]string{ 103 | "*", 104 | "-owner", 105 | "-owner2", 106 | "+owner", 107 | }) 108 | expect("", true) 109 | expect("a", true) 110 | expect("owner", true) 111 | expect("owner2", false) 112 | expect("a/1", true) 113 | expect("owner/1", true) 114 | expect("owner2/1", false) 115 | 116 | config([]string{ 117 | "-*", 118 | "+owner", 119 | "+owner2", 120 | }) 121 | expect("", false) 122 | expect("a", false) 123 | expect("owner", true) 124 | expect("owner2", true) 125 | expect("a/1", false) 126 | expect("owner/1", true) 127 | expect("owner2/1", true) 128 | 129 | config([]string{ 130 | "-*", 131 | "+owner", 132 | "+owner2", 133 | "-owner", 134 | }) 135 | expect("", false) 136 | expect("a", false) 137 | expect("owner", false) 138 | expect("owner2", true) 139 | expect("a/1", false) 140 | expect("owner/1", false) 141 | expect("owner2/1", true) 142 | 143 | config([]string{ 144 | "owner/repo", 145 | }) 146 | expect("", false) 147 | expect("a", false) 148 | expect("owner", true) 149 | expect("a/1", false) 150 | expect("owner/1", false) 151 | expect("owner/repo", true) 152 | 153 | config([]string{ 154 | "owner/repo", 155 | "owner2", 156 | }) 157 | expect("", false) 158 | expect("a", false) 159 | expect("owner", true) 160 | expect("owner2", true) 161 | expect("a/1", false) 162 | expect("owner/1", false) 163 | expect("owner/repo", true) 164 | expect("owner2/repo", true) 165 | 166 | config([]string{ 167 | "-owner/repo", 168 | }) 169 | expect("", false) 170 | expect("a", false) 171 | expect("owner", false) 172 | expect("a/1", false) 173 | expect("owner/1", false) 174 | expect("owner/repo", false) 175 | 176 | config([]string{ 177 | "*", 178 | "-owner/repo", 179 | }) 180 | expect("", true) 181 | expect("a", true) 182 | expect("owner", true) 183 | expect("a/1", true) 184 | expect("owner/1", true) 185 | expect("owner/repo", false) 186 | } 187 | -------------------------------------------------------------------------------- /src/prov/git_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * git_test.go 3 | * 4 | * Copyright 2021-2022 Bill Zissimopoulos 5 | */ 6 | /* 7 | * This file is part of Hubfs. 8 | * 9 | * You can redistribute it and/or modify it under the terms of the GNU 10 | * Affero General Public License version 3 as published by the Free 11 | * Software Foundation. 12 | */ 13 | 14 | package prov 15 | 16 | import ( 17 | "bytes" 18 | "io" 19 | "io/ioutil" 20 | "os" 21 | "runtime" 22 | "testing" 23 | 24 | "github.com/billziss-gh/golib/keyring" 25 | ) 26 | 27 | const remote = "https://github.com/winfsp/hubfs" 28 | const refName = "master" 29 | const tagName = "v1.0B1" 30 | const entryName = "README.md" 31 | const subtreeName = "src" 32 | const subentryName = "go.mod" 33 | const commitName = "865aad06c4ecde192460b429f810bb84c0d9ca7b" 34 | 35 | var testRepository Repository 36 | var caseins bool 37 | 38 | func TestGetRefs(t *testing.T) { 39 | refs, err := testRepository.GetRefs() 40 | if nil != err { 41 | t.Error(err) 42 | } 43 | found := false 44 | for _, ref := range refs { 45 | if ref.Name() == refName { 46 | found = true 47 | break 48 | } 49 | } 50 | if !found { 51 | t.Error() 52 | } 53 | 54 | refs, err = testRepository.GetRefs() 55 | if nil != err { 56 | t.Error(err) 57 | } 58 | found = false 59 | for _, ref := range refs { 60 | if ref.Name() == refName { 61 | found = true 62 | break 63 | } 64 | } 65 | if !found { 66 | t.Error() 67 | } 68 | } 69 | 70 | func TestGetRef(t *testing.T) { 71 | ref, err := testRepository.GetRef(refName) 72 | if nil != err { 73 | t.Error(err) 74 | } 75 | if ref.Name() != refName { 76 | t.Error() 77 | } 78 | 79 | ref, err = testRepository.GetRef(refName) 80 | if nil != err { 81 | t.Error(err) 82 | } 83 | if ref.Name() != refName { 84 | t.Error() 85 | } 86 | } 87 | 88 | func TestGetTempRef(t *testing.T) { 89 | ref, err := testRepository.GetTempRef(commitName) 90 | if nil != err { 91 | t.Error(err) 92 | } 93 | if ref.Name() != commitName { 94 | t.Error() 95 | } 96 | 97 | ref, err = testRepository.GetTempRef(commitName) 98 | if nil != err { 99 | t.Error(err) 100 | } 101 | if ref.Name() != commitName { 102 | t.Error() 103 | } 104 | } 105 | 106 | func testGetRefTree(t *testing.T, name string) { 107 | ref, err := testRepository.GetRef(name) 108 | if nil != err { 109 | t.Error(err) 110 | } 111 | if ref.Name() != name { 112 | t.Error() 113 | } 114 | 115 | tree, err := testRepository.GetTree(ref, nil) 116 | if nil != err { 117 | t.Error(err) 118 | } 119 | found := false 120 | for _, entry := range tree { 121 | if entry.Name() == entryName { 122 | found = true 123 | break 124 | } 125 | } 126 | if !found { 127 | t.Error() 128 | } 129 | 130 | tree, err = testRepository.GetTree(ref, nil) 131 | if nil != err { 132 | t.Error(err) 133 | } 134 | found = false 135 | for _, entry := range tree { 136 | if entry.Name() == entryName { 137 | found = true 138 | break 139 | } 140 | } 141 | if !found { 142 | t.Error() 143 | } 144 | } 145 | 146 | func TestGetRefTree(t *testing.T) { 147 | testGetRefTree(t, refName) 148 | testGetRefTree(t, tagName) 149 | } 150 | 151 | func testGetRefTreeEntry(t *testing.T, name string) { 152 | ref, err := testRepository.GetRef(name) 153 | if nil != err { 154 | t.Error(err) 155 | } 156 | if ref.Name() != name { 157 | t.Error() 158 | } 159 | 160 | entry, err := testRepository.GetTreeEntry(ref, nil, entryName) 161 | if nil != err { 162 | t.Error(err) 163 | } 164 | if entry.Name() != entryName { 165 | t.Error() 166 | } 167 | 168 | entry, err = testRepository.GetTreeEntry(ref, nil, entryName) 169 | if nil != err { 170 | t.Error(err) 171 | } 172 | if entry.Name() != entryName { 173 | t.Error() 174 | } 175 | 176 | } 177 | 178 | func TestGetRefTreeEntry(t *testing.T) { 179 | testGetRefTreeEntry(t, refName) 180 | testGetRefTreeEntry(t, tagName) 181 | } 182 | 183 | func testGetTree(t *testing.T, name string) { 184 | ref, err := testRepository.GetRef(name) 185 | if nil != err { 186 | t.Error(err) 187 | } 188 | if ref.Name() != name { 189 | t.Error() 190 | } 191 | 192 | entry, err := testRepository.GetTreeEntry(ref, nil, subtreeName) 193 | if nil != err { 194 | t.Error(err) 195 | } 196 | if entry.Name() != subtreeName { 197 | t.Error() 198 | } 199 | 200 | tree, err := testRepository.GetTree(nil, entry) 201 | if nil != err { 202 | t.Error(err) 203 | } 204 | found := false 205 | for _, entry := range tree { 206 | if entry.Name() == subentryName { 207 | found = true 208 | break 209 | } 210 | } 211 | if !found { 212 | t.Error() 213 | } 214 | 215 | tree, err = testRepository.GetTree(nil, entry) 216 | if nil != err { 217 | t.Error(err) 218 | } 219 | found = false 220 | for _, entry := range tree { 221 | if entry.Name() == subentryName { 222 | found = true 223 | break 224 | } 225 | } 226 | if !found { 227 | t.Error() 228 | } 229 | } 230 | 231 | func TestGetTree(t *testing.T) { 232 | testGetTree(t, refName) 233 | testGetTree(t, tagName) 234 | } 235 | 236 | func testGetTreeEntry(t *testing.T, name string) { 237 | ref, err := testRepository.GetRef(name) 238 | if nil != err { 239 | t.Error(err) 240 | } 241 | if ref.Name() != name { 242 | t.Error() 243 | } 244 | 245 | entry, err := testRepository.GetTreeEntry(ref, nil, subtreeName) 246 | if nil != err { 247 | t.Error(err) 248 | } 249 | if entry.Name() != subtreeName { 250 | t.Error() 251 | } 252 | 253 | subentry, err := testRepository.GetTreeEntry(nil, entry, subentryName) 254 | if nil != err { 255 | t.Error(err) 256 | } 257 | if subentry.Name() != subentryName { 258 | t.Error() 259 | } 260 | 261 | subentry, err = testRepository.GetTreeEntry(nil, entry, subentryName) 262 | if nil != err { 263 | t.Error(err) 264 | } 265 | if subentry.Name() != subentryName { 266 | t.Error() 267 | } 268 | } 269 | 270 | func TestGetTreeEntry(t *testing.T) { 271 | testGetTreeEntry(t, refName) 272 | testGetTreeEntry(t, tagName) 273 | } 274 | 275 | func TestGetBlobReader(t *testing.T) { 276 | ref, err := testRepository.GetRef(refName) 277 | if nil != err { 278 | t.Error(err) 279 | } 280 | if ref.Name() != refName { 281 | t.Error() 282 | } 283 | 284 | entry, err := testRepository.GetTreeEntry(ref, nil, subtreeName) 285 | if nil != err { 286 | t.Error(err) 287 | } 288 | if entry.Name() != subtreeName { 289 | t.Error() 290 | } 291 | 292 | subentry, err := testRepository.GetTreeEntry(nil, entry, subentryName) 293 | if nil != err { 294 | t.Error(err) 295 | } 296 | if subentry.Name() != subentryName { 297 | t.Error() 298 | } 299 | 300 | reader, err := testRepository.GetBlobReader(subentry) 301 | if nil != err { 302 | t.Error(err) 303 | } 304 | content, err := ioutil.ReadAll(reader.(io.Reader)) 305 | reader.(io.Closer).Close() 306 | if !bytes.Contains(content, []byte("module github.com")) { 307 | t.Error() 308 | } 309 | 310 | reader, err = testRepository.GetBlobReader(subentry) 311 | if nil != err { 312 | t.Error(err) 313 | } 314 | content, err = ioutil.ReadAll(reader.(io.Reader)) 315 | reader.(io.Closer).Close() 316 | if !bytes.Contains(content, []byte("module github.com")) { 317 | t.Error() 318 | } 319 | } 320 | 321 | func TestGetModule(t *testing.T) { 322 | const remote = "https://github.com/winfsp/winfsp" 323 | const refName = "master" 324 | const modulePath = "ext/test" 325 | const moduleTarget = "/billziss-gh/secfs.test" 326 | 327 | repository, err := NewGitRepository(remote, "", "", caseins, false) 328 | if nil != err { 329 | t.Error(err) 330 | } 331 | defer repository.Close() 332 | 333 | ref, err := repository.GetRef(refName) 334 | if nil != err { 335 | t.Error(err) 336 | } 337 | if ref.Name() != refName { 338 | t.Error() 339 | } 340 | 341 | module, err := repository.GetModule(ref, modulePath, true) 342 | if nil != err { 343 | t.Error(err) 344 | } 345 | if module != moduleTarget { 346 | t.Error() 347 | } 348 | 349 | module, err = repository.GetModule(ref, modulePath, true) 350 | if nil != err { 351 | t.Error(err) 352 | } 353 | if module != moduleTarget { 354 | t.Error() 355 | } 356 | } 357 | 358 | func init() { 359 | atinit(func() error { 360 | if "windows" == runtime.GOOS || "darwin" == runtime.GOOS { 361 | caseins = true 362 | } 363 | 364 | token, err := keyring.Get("hubfs", "github.com") 365 | if nil != err { 366 | token = "" 367 | } 368 | if "" == token { 369 | token = os.Getenv("HUBFS_TOKEN") 370 | } 371 | 372 | testRepository, err = NewGitRepository(remote, token, "x-oauth-basic", caseins, false) 373 | if nil != err { 374 | return err 375 | } 376 | 377 | tdir, err := ioutil.TempDir("", "git_test") 378 | if nil != err { 379 | return err 380 | } 381 | 382 | err = testRepository.SetDirectory(tdir) 383 | if nil != err { 384 | return err 385 | } 386 | 387 | atexit(func() { 388 | testRepository.RemoveDirectory() 389 | testRepository.Close() 390 | }) 391 | 392 | return nil 393 | }) 394 | } 395 | -------------------------------------------------------------------------------- /src/prov/github.go: -------------------------------------------------------------------------------- 1 | /* 2 | * github.go 3 | * 4 | * Copyright 2021-2022 Bill Zissimopoulos 5 | */ 6 | /* 7 | * This file is part of Hubfs. 8 | * 9 | * You can redistribute it and/or modify it under the terms of the GNU 10 | * Affero General Public License version 3 as published by the Free 11 | * Software Foundation. 12 | */ 13 | 14 | package prov 15 | 16 | import ( 17 | "bytes" 18 | "encoding/json" 19 | "errors" 20 | "fmt" 21 | "net/http" 22 | "net/url" 23 | pathutil "path" 24 | "strings" 25 | 26 | "github.com/cli/oauth" 27 | "github.com/winfsp/hubfs/httputil" 28 | ) 29 | 30 | type GithubProvider struct { 31 | Hostname string 32 | ClientId string 33 | ClientSecret string 34 | CallbackURI string 35 | Scopes string 36 | ApiURI string 37 | } 38 | 39 | func NewGithubComProvider(uri *url.URL) Provider { 40 | return &GithubProvider{ 41 | Hostname: "github.com", 42 | ClientId: "4c24e0557d7103e3c4b0", // safe to embed 43 | ClientSecret: "ClientSecret", 44 | CallbackURI: "http://127.0.0.1/callback", 45 | Scopes: "repo", 46 | ApiURI: "https://api.github.com", 47 | } 48 | } 49 | 50 | func init() { 51 | RegisterProviderClass("github.com", NewGithubComProvider, ""+ 52 | "[https://]github.com[/owner[/repo]]\n"+ 53 | " \taccess github.com\n"+ 54 | " \t- owner file system root is at owner\n"+ 55 | " \t- repo file system root is at owner/repo") 56 | } 57 | 58 | func (p *GithubProvider) Auth() (token string, err error) { 59 | flow := &oauth.Flow{ 60 | Host: oauth.GitHubHost("https://" + p.Hostname), 61 | ClientID: p.ClientId, 62 | ClientSecret: p.ClientSecret, 63 | CallbackURI: p.CallbackURI, 64 | Scopes: strings.Split(p.Scopes, ","), 65 | HTTPClient: httputil.DefaultClient, 66 | } 67 | accessToken, err := flow.DetectFlow() 68 | if nil != accessToken { 69 | token = accessToken.Token 70 | } 71 | return 72 | } 73 | 74 | func (p *GithubProvider) NewClient(token string) (Client, error) { 75 | return NewGithubClient(p.ApiURI, token) 76 | } 77 | 78 | type githubClient struct { 79 | client 80 | httpClient *http.Client 81 | ident string 82 | apiURI string 83 | gqlApiURI string 84 | token string 85 | login string 86 | } 87 | 88 | func NewGithubClient(apiURI string, token string) (Client, error) { 89 | uri, err := url.Parse(apiURI) 90 | if nil != err { 91 | return nil, err 92 | } 93 | 94 | c := &githubClient{ 95 | httpClient: httputil.DefaultClient, 96 | ident: uri.Hostname(), 97 | apiURI: apiURI, 98 | gqlApiURI: apiURI + "/graphql", 99 | token: token, 100 | } 101 | c.client.init(c) 102 | 103 | if m, _ := pathutil.Match("/api/v*", uri.Path); m { 104 | c.gqlApiURI = uri.Scheme + "://" + uri.Host + "/api/graphql" 105 | } 106 | 107 | if "" != c.token { 108 | rsp, err := c.sendrecv("/user") 109 | if nil != err { 110 | return nil, err 111 | } 112 | defer rsp.Body.Close() 113 | 114 | var content struct { 115 | Login string `json:"login"` 116 | } 117 | err = json.NewDecoder(rsp.Body).Decode(&content) 118 | if nil != err { 119 | return nil, err 120 | } 121 | 122 | c.login = content.Login 123 | } 124 | 125 | return c, nil 126 | } 127 | 128 | func (c *githubClient) getIdent() string { 129 | return c.ident 130 | } 131 | 132 | func (c *githubClient) getGitCredentials() (string, string) { 133 | return c.token, "x-oauth-basic" 134 | } 135 | 136 | func (c *githubClient) sendrecv(path string) (*http.Response, error) { 137 | req, err := http.NewRequest("GET", c.apiURI+path, nil) 138 | if nil != err { 139 | return nil, err 140 | } 141 | 142 | req.Header.Set("Accept", "application/vnd.github.v3+json") 143 | if "" != c.token { 144 | req.Header.Set("Authorization", "token "+c.token) 145 | } 146 | 147 | rsp, err := c.httpClient.Do(req) 148 | if nil != err { 149 | return nil, err 150 | } 151 | 152 | if 404 == rsp.StatusCode { 153 | return nil, ErrNotFound 154 | } else if 400 <= rsp.StatusCode { 155 | return nil, errors.New(fmt.Sprintf("HTTP %d", rsp.StatusCode)) 156 | } 157 | 158 | return rsp, nil 159 | } 160 | 161 | func (c *githubClient) sendrecvGql(query string) (*http.Response, error) { 162 | var content = struct { 163 | Query string `json:"query"` 164 | }{ 165 | Query: query, 166 | } 167 | var body bytes.Buffer 168 | err := json.NewEncoder(&body).Encode(&content) 169 | if nil != err { 170 | return nil, err 171 | } 172 | 173 | req, err := http.NewRequest("POST", c.gqlApiURI, &body) 174 | if nil != err { 175 | return nil, err 176 | } 177 | 178 | req.Header.Set("Content-type", "application/json") 179 | if "" != c.token { 180 | req.Header.Set("Authorization", "token "+c.token) 181 | } 182 | 183 | rsp, err := c.httpClient.Do(req) 184 | if nil != err { 185 | return nil, err 186 | } 187 | 188 | if 404 == rsp.StatusCode { 189 | return nil, ErrNotFound 190 | } else if 400 <= rsp.StatusCode { 191 | return nil, errors.New(fmt.Sprintf("HTTP %d", rsp.StatusCode)) 192 | } 193 | 194 | return rsp, nil 195 | } 196 | 197 | func (c *githubClient) getOwner(o string) (res *owner, err error) { 198 | defer trace(o)(&err) 199 | 200 | rsp, err := c.sendrecv(fmt.Sprintf("/users/%s", url.PathEscape(o))) 201 | if nil != err { 202 | return nil, err 203 | } 204 | defer rsp.Body.Close() 205 | 206 | var content struct { 207 | FName string `json:"login"` 208 | FKind string `json:"type"` 209 | } 210 | err = json.NewDecoder(rsp.Body).Decode(&content) 211 | if nil != err { 212 | return nil, err 213 | } 214 | 215 | res = &owner{ 216 | FName: content.FName, 217 | FKind: content.FKind, 218 | } 219 | res.Value = res 220 | return 221 | } 222 | 223 | func (c *githubClient) getRepositoryPageRest(path string) ([]*repository, error) { 224 | rsp, err := c.sendrecv(path) 225 | if nil != err { 226 | return nil, err 227 | } 228 | defer rsp.Body.Close() 229 | 230 | var content []struct { 231 | FName string `json:"name"` 232 | FRemote string `json:"clone_url"` 233 | } 234 | err = json.NewDecoder(rsp.Body).Decode(&content) 235 | if nil != err { 236 | return nil, err 237 | } 238 | 239 | res := make([]*repository, len(content)) 240 | for i, elm := range content { 241 | r := &repository{ 242 | FName: elm.FName, 243 | FRemote: elm.FRemote, 244 | } 245 | r.Value = r 246 | r.Repository = emptyRepository 247 | r.keepdir = c.keepdir 248 | res[i] = r 249 | } 250 | 251 | return res, nil 252 | } 253 | 254 | func (c *githubClient) getRepositoriesRest(owner string, kind string) (res []*repository, err error) { 255 | defer trace(owner)(&err) 256 | 257 | var path string 258 | if "Organization" == kind { 259 | path = fmt.Sprintf("/orgs/%s/repos?type=all&per_page=100", url.PathEscape(owner)) 260 | } else if c.login == owner { 261 | path = "/user/repos?visibility=all&affiliation=owner&per_page=100" 262 | } else { 263 | path = fmt.Sprintf("/users/%s/repos?type=owner&per_page=100", url.PathEscape(owner)) 264 | } 265 | 266 | res = make([]*repository, 0) 267 | for page := 1; ; page++ { 268 | lst, err := c.getRepositoryPageRest(path + fmt.Sprintf("&page=%d", page)) 269 | if nil != err { 270 | return nil, err 271 | } 272 | res = append(res, lst...) 273 | if len(lst) < 100 { 274 | break 275 | } 276 | } 277 | 278 | return res, nil 279 | } 280 | 281 | func (c *githubClient) getRepositoryPageGql(query string) ([]*repository, string, error) { 282 | rsp, err := c.sendrecvGql(query) 283 | if nil != err { 284 | return nil, "", err 285 | } 286 | defer rsp.Body.Close() 287 | 288 | var content struct { 289 | Data struct { 290 | Owner struct { 291 | Repositories struct { 292 | PageInfo struct { 293 | HasNextPage bool `json:"hasNextPage"` 294 | EndCursor string `json:"endCursor"` 295 | } `json:"pageInfo"` 296 | Nodes []struct { 297 | FName string `json:"name"` 298 | FRemote string `json:"url"` 299 | } `json:"nodes"` 300 | } `json:"repositories"` 301 | } `json:"owner"` 302 | } `json:"data"` 303 | Errors []struct { 304 | Message string `json:"message"` 305 | } `json:"errors"` 306 | } 307 | err = json.NewDecoder(rsp.Body).Decode(&content) 308 | if nil != err { 309 | return nil, "", err 310 | } 311 | if 0 < len(content.Errors) { 312 | return nil, "", errors.New(fmt.Sprintf("GraphQL: %s", content.Errors[0].Message)) 313 | } 314 | 315 | res := make([]*repository, len(content.Data.Owner.Repositories.Nodes)) 316 | for i, elm := range content.Data.Owner.Repositories.Nodes { 317 | r := &repository{ 318 | FName: elm.FName, 319 | FRemote: elm.FRemote, 320 | } 321 | r.Value = r 322 | r.Repository = emptyRepository 323 | r.keepdir = c.keepdir 324 | res[i] = r 325 | } 326 | 327 | crs := "" 328 | if content.Data.Owner.Repositories.PageInfo.HasNextPage { 329 | crs = content.Data.Owner.Repositories.PageInfo.EndCursor 330 | } 331 | 332 | return res, crs, nil 333 | } 334 | 335 | func (c *githubClient) getRepositoriesGql(owner string, kind string) (res []*repository, err error) { 336 | defer trace(owner)(&err) 337 | 338 | query := `{ 339 | owner: %s { 340 | repositories(ownerAffiliations: OWNER, first: 100%%s) { 341 | pageInfo { 342 | hasNextPage 343 | endCursor 344 | } 345 | nodes { 346 | name 347 | url 348 | } 349 | } 350 | } 351 | }` 352 | 353 | if c.login == owner { 354 | query = fmt.Sprintf(query, "viewer") 355 | } else { 356 | query = fmt.Sprintf(query, `repositoryOwner(login: "`+owner+`")`) 357 | } 358 | 359 | res = make([]*repository, 0) 360 | var lst []*repository 361 | var crs string 362 | for { 363 | if "" != crs { 364 | crs = `, after: "` + crs + `"` 365 | } 366 | lst, crs, err = c.getRepositoryPageGql(fmt.Sprintf(query, crs)) 367 | if nil != err { 368 | return nil, err 369 | } 370 | res = append(res, lst...) 371 | if "" == crs { 372 | break 373 | } 374 | } 375 | 376 | return res, nil 377 | } 378 | 379 | func (c *githubClient) getRepositories(owner string, kind string) (res []*repository, err error) { 380 | if "" != c.token { 381 | /* 382 | * Attempt to list repositories via a GraphQL query because they are much faster for large 383 | * listings than REST. For example, listing the GitHub microsoft account takes 1m26s(!) 384 | * using REST, but "only" 18s using GraphQL. 385 | * 386 | * There are however some problems with using GraphQL: 387 | * 388 | * 1. GraphQL requires authentication. 389 | * 390 | * 2. Even with authentication GraphQL queries can sometimes fail with OAuth credentials. 391 | * The following error message is possible: "Although you appear to have the correct 392 | * authorization credentials, the `NAME` organization has enabled OAuth App access 393 | * restrictions, meaning that data access to third-parties is limited. For more information 394 | * on these restrictions, including how to enable this app, visit 395 | * https://docs.github.com/articles/restricting-access-to-your-organization-s-data/" 396 | * 397 | * For this reason GraphQL queries are not reliable and we always fall back to REST when 398 | * encountering an error. 399 | * 400 | * An alternative solution to this problem was using multiple concurrent requests to fetch 401 | * the listing pages. Unfortunately the GitHub API discourages such use, because of 402 | * secondary rate limiting: 403 | * https://docs.github.com/en/rest/overview/resources-in-the-rest-api#secondary-rate-limits. 404 | */ 405 | res, err = c.getRepositoriesGql(owner, kind) 406 | if nil == err { 407 | return 408 | } 409 | } 410 | return c.getRepositoriesRest(owner, kind) 411 | } 412 | -------------------------------------------------------------------------------- /src/prov/github_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * github_test.go 3 | * 4 | * Copyright 2021-2022 Bill Zissimopoulos 5 | */ 6 | /* 7 | * This file is part of Hubfs. 8 | * 9 | * You can redistribute it and/or modify it under the terms of the GNU 10 | * Affero General Public License version 3 as published by the Free 11 | * Software Foundation. 12 | */ 13 | 14 | package prov 15 | 16 | import ( 17 | "net/url" 18 | "os" 19 | "testing" 20 | "time" 21 | 22 | "github.com/billziss-gh/golib/keyring" 23 | ) 24 | 25 | const ownerName = "winfsp" 26 | const repositoryName = "hubfs" 27 | 28 | var testClient Client 29 | 30 | func TestOpenCloseOwner(t *testing.T) { 31 | owner, err := testClient.OpenOwner(ownerName) 32 | if nil != err { 33 | t.Error(err) 34 | } 35 | if owner.Name() != ownerName { 36 | t.Error() 37 | } 38 | testClient.CloseOwner(owner) 39 | 40 | owner, err = testClient.OpenOwner(ownerName) 41 | if nil != err { 42 | t.Error(err) 43 | } 44 | if owner.Name() != ownerName { 45 | t.Error() 46 | } 47 | testClient.CloseOwner(owner) 48 | } 49 | 50 | func TestGetRepositories(t *testing.T) { 51 | owner, err := testClient.OpenOwner(ownerName) 52 | if nil != err { 53 | t.Error(err) 54 | } 55 | defer testClient.CloseOwner(owner) 56 | if owner.Name() != ownerName { 57 | t.Error() 58 | } 59 | 60 | repositories, err := testClient.GetRepositories(owner) 61 | if nil != err { 62 | t.Error(err) 63 | } 64 | found := false 65 | for _, e := range repositories { 66 | if e.Name() == repositoryName { 67 | found = true 68 | break 69 | } 70 | } 71 | if !found { 72 | t.Error() 73 | } 74 | 75 | repositories, err = testClient.GetRepositories(owner) 76 | if nil != err { 77 | t.Error(err) 78 | } 79 | found = false 80 | for _, e := range repositories { 81 | if e.Name() == repositoryName { 82 | found = true 83 | break 84 | } 85 | } 86 | if !found { 87 | t.Error() 88 | } 89 | } 90 | 91 | func TestOpenCloseRepository(t *testing.T) { 92 | owner, err := testClient.OpenOwner(ownerName) 93 | if nil != err { 94 | t.Error(err) 95 | } 96 | defer testClient.CloseOwner(owner) 97 | if owner.Name() != ownerName { 98 | t.Error() 99 | } 100 | 101 | repository, err := testClient.OpenRepository(owner, repositoryName) 102 | if nil != err { 103 | t.Error(err) 104 | } 105 | if repository.Name() != repositoryName { 106 | t.Error() 107 | } 108 | testClient.CloseRepository(repository) 109 | 110 | repository, err = testClient.OpenRepository(owner, repositoryName) 111 | if nil != err { 112 | t.Error(err) 113 | } 114 | if repository.Name() != repositoryName { 115 | t.Error() 116 | } 117 | testClient.CloseRepository(repository) 118 | } 119 | 120 | func testExpiration(t *testing.T) { 121 | testClient.StartExpiration() 122 | defer testClient.StopExpiration() 123 | 124 | owner, err := testClient.OpenOwner(ownerName) 125 | if nil != err { 126 | t.Error(err) 127 | } 128 | if owner.Name() != ownerName { 129 | t.Error() 130 | } 131 | 132 | repository, err := testClient.OpenRepository(owner, repositoryName) 133 | if nil != err { 134 | t.Error(err) 135 | } 136 | if repository.Name() != repositoryName { 137 | t.Error() 138 | } 139 | 140 | testClient.CloseRepository(repository) 141 | testClient.CloseOwner(owner) 142 | 143 | time.Sleep(3 * time.Second) 144 | 145 | owner, err = testClient.OpenOwner(ownerName) 146 | if nil != err { 147 | t.Error(err) 148 | } 149 | if owner.Name() != ownerName { 150 | t.Error() 151 | } 152 | 153 | repository, err = testClient.OpenRepository(owner, repositoryName) 154 | if nil != err { 155 | t.Error(err) 156 | } 157 | if repository.Name() != repositoryName { 158 | t.Error() 159 | } 160 | 161 | testClient.CloseRepository(repository) 162 | testClient.CloseOwner(owner) 163 | } 164 | 165 | func TestExpiration(t *testing.T) { 166 | testExpiration(t) 167 | testExpiration(t) 168 | } 169 | 170 | func init() { 171 | atinit(func() error { 172 | token, err := keyring.Get("hubfs", "github.com") 173 | if nil != err { 174 | token = "" 175 | } 176 | if "" == token { 177 | token = os.Getenv("HUBFS_TOKEN") 178 | } 179 | 180 | uri, _ := url.Parse("https://github.com") 181 | testClient, err = NewProviderInstance(uri).NewClient(token) 182 | if nil != err { 183 | return err 184 | } 185 | 186 | testClient.SetConfig([]string{"config.ttl=1s"}) 187 | 188 | return nil 189 | }) 190 | } 191 | -------------------------------------------------------------------------------- /src/prov/gitlab.go: -------------------------------------------------------------------------------- 1 | /* 2 | * gitlab.go 3 | * 4 | * Copyright 2021-2022 Bill Zissimopoulos 5 | */ 6 | /* 7 | * This file is part of Hubfs. 8 | * 9 | * You can redistribute it and/or modify it under the terms of the GNU 10 | * Affero General Public License version 3 as published by the Free 11 | * Software Foundation. 12 | */ 13 | 14 | package prov 15 | 16 | import ( 17 | "crypto/rand" 18 | "crypto/sha256" 19 | "encoding/base64" 20 | "encoding/json" 21 | "errors" 22 | "fmt" 23 | "net/http" 24 | "net/url" 25 | "strings" 26 | 27 | "github.com/cli/browser" 28 | "github.com/cli/oauth" 29 | "github.com/winfsp/hubfs/httputil" 30 | ) 31 | 32 | type GitlabProvider struct { 33 | Hostname string 34 | ClientId string 35 | ClientSecret string 36 | CallbackURI string 37 | Scopes string 38 | ApiURI string 39 | } 40 | 41 | func NewGitlabComProvider(uri *url.URL) Provider { 42 | return &GitlabProvider{ 43 | Hostname: "gitlab.com", 44 | ClientId: "034c04be0f0e17bc02fca5dce2b3448fde93a1c06f1a1187825e7140ebc58118", // safe to embed 45 | ClientSecret: "ClientSecret", 46 | CallbackURI: "http://127.0.0.1/callback", 47 | Scopes: "read_api,read_user,read_repository", 48 | ApiURI: "https://gitlab.com/api/v4", 49 | } 50 | } 51 | 52 | func init() { 53 | RegisterProviderClass("gitlab.com", NewGitlabComProvider, ""+ 54 | "[https://]gitlab.com[/owner[/repo]]\n"+ 55 | " \taccess gitlab.com\n"+ 56 | " \t- owner file system root is at owner\n"+ 57 | " \t- repo file system root is at owner/repo") 58 | } 59 | 60 | type gitlabWebAppFlowHttpClient struct { 61 | *http.Client 62 | callbackURI string 63 | code_verifier string 64 | } 65 | 66 | func (c *gitlabWebAppFlowHttpClient) PostForm(url string, data url.Values) (*http.Response, error) { 67 | data.Del("client_secret") 68 | data.Set("grant_type", "authorization_code") 69 | data.Set("redirect_uri", c.callbackURI) 70 | data.Set("code_verifier", c.code_verifier) 71 | return c.Client.PostForm(url, data) 72 | } 73 | 74 | func (p *GitlabProvider) Auth() (token string, err error) { 75 | // PKCE (RFC 7636) for GitLab 76 | buf := make([]byte, 80) 77 | _, err = rand.Read(buf) 78 | if nil != err { 79 | return "", err 80 | } 81 | b64 := make([]byte, base64.RawURLEncoding.EncodedLen(len(buf))) 82 | base64.RawURLEncoding.Encode(b64, buf) 83 | sum := sha256.Sum256(b64) 84 | code_verifier := string(b64) 85 | code_challenge := base64.RawURLEncoding.EncodeToString(sum[:]) 86 | 87 | flow := &oauth.Flow{ 88 | Host: &oauth.Host{ 89 | AuthorizeURL: fmt.Sprintf("https://%s/oauth/authorize", p.Hostname), 90 | TokenURL: fmt.Sprintf("https://%s/oauth/token", p.Hostname), 91 | }, 92 | ClientID: p.ClientId, 93 | ClientSecret: p.ClientSecret, 94 | CallbackURI: p.CallbackURI, 95 | Scopes: strings.Split(p.Scopes, ","), 96 | BrowseURL: func(uri string) error { 97 | return browser.OpenURL( 98 | fmt.Sprintf("%s&response_type=code&code_challenge=%s&code_challenge_method=S256", 99 | uri, code_challenge)) 100 | }, 101 | HTTPClient: &gitlabWebAppFlowHttpClient{ 102 | Client: httputil.DefaultClient, 103 | callbackURI: p.CallbackURI, 104 | code_verifier: code_verifier, 105 | }, 106 | } 107 | accessToken, err := flow.WebAppFlow() 108 | if nil != accessToken { 109 | token = accessToken.Token 110 | } 111 | return 112 | } 113 | 114 | func (p *GitlabProvider) NewClient(token string) (Client, error) { 115 | return NewGitlabClient(p.ApiURI, token) 116 | } 117 | 118 | type gitlabClient struct { 119 | client 120 | httpClient *http.Client 121 | ident string 122 | apiURI string 123 | token string 124 | login string 125 | } 126 | 127 | func NewGitlabClient(apiURI string, token string) (Client, error) { 128 | uri, err := url.Parse(apiURI) 129 | if nil != err { 130 | return nil, err 131 | } 132 | 133 | c := &gitlabClient{ 134 | httpClient: httputil.DefaultClient, 135 | ident: uri.Hostname(), 136 | apiURI: apiURI, 137 | token: token, 138 | } 139 | c.client.init(c) 140 | 141 | if "" != c.token { 142 | rsp, err := c.sendrecv("/user") 143 | if nil != err { 144 | return nil, err 145 | } 146 | defer rsp.Body.Close() 147 | 148 | var content struct { 149 | Login string `json:"username"` 150 | } 151 | err = json.NewDecoder(rsp.Body).Decode(&content) 152 | if nil != err { 153 | return nil, err 154 | } 155 | 156 | c.login = content.Login 157 | } 158 | 159 | return c, nil 160 | } 161 | 162 | func (c *gitlabClient) getIdent() string { 163 | return c.ident 164 | } 165 | 166 | func (c *gitlabClient) getGitCredentials() (string, string) { 167 | return "oauth2", c.token 168 | } 169 | 170 | func (c *gitlabClient) sendrecv(path string) (*http.Response, error) { 171 | req, err := http.NewRequest("GET", c.apiURI+path, nil) 172 | if nil != err { 173 | return nil, err 174 | } 175 | 176 | if "" != c.token { 177 | req.Header.Set("Authorization", "Bearer "+c.token) 178 | } 179 | 180 | rsp, err := c.httpClient.Do(req) 181 | if nil != err { 182 | return nil, err 183 | } 184 | 185 | if 404 == rsp.StatusCode { 186 | return nil, ErrNotFound 187 | } else if 400 <= rsp.StatusCode { 188 | return nil, errors.New(fmt.Sprintf("HTTP %d", rsp.StatusCode)) 189 | } 190 | 191 | return rsp, nil 192 | } 193 | 194 | func (c *gitlabClient) getUser(o string) (res *owner, err error) { 195 | defer trace(o)(&err) 196 | 197 | rsp, err := c.sendrecv(fmt.Sprintf("/users?username=%s", url.PathEscape(o))) 198 | if nil != err { 199 | return nil, err 200 | } 201 | defer rsp.Body.Close() 202 | 203 | var content []struct { 204 | FName string `json:"username"` 205 | } 206 | err = json.NewDecoder(rsp.Body).Decode(&content) 207 | if nil != err { 208 | return nil, err 209 | } 210 | if 0 == len(content) { 211 | return nil, ErrNotFound 212 | } 213 | 214 | res = &owner{ 215 | FName: content[0].FName, 216 | FKind: "user", 217 | } 218 | res.Value = res 219 | return 220 | } 221 | 222 | func (c *gitlabClient) getGroup(o string) (res *owner, err error) { 223 | defer trace(o)(&err) 224 | 225 | rsp, err := c.sendrecv(fmt.Sprintf("/groups/%s?with_projects=false", url.PathEscape(o))) 226 | if nil != err { 227 | return nil, err 228 | } 229 | defer rsp.Body.Close() 230 | 231 | var content struct { 232 | FName string `json:"path"` 233 | } 234 | err = json.NewDecoder(rsp.Body).Decode(&content) 235 | if nil != err { 236 | return nil, err 237 | } 238 | 239 | res = &owner{ 240 | FName: content.FName, 241 | FKind: "group", 242 | } 243 | res.Value = res 244 | return 245 | } 246 | 247 | func (c *gitlabClient) getOwner(o string) (res *owner, err error) { 248 | res, err = c.getUser(o) 249 | if ErrNotFound != err { 250 | return 251 | } 252 | res, err = c.getGroup(o) 253 | return 254 | } 255 | 256 | func (c *gitlabClient) getRepositoryPage(prefix string, path string) ([]*repository, error) { 257 | rsp, err := c.sendrecv(path) 258 | if nil != err { 259 | return nil, err 260 | } 261 | defer rsp.Body.Close() 262 | 263 | var content []struct { 264 | FName string `json:"path_with_namespace"` 265 | FRemote string `json:"http_url_to_repo"` 266 | } 267 | err = json.NewDecoder(rsp.Body).Decode(&content) 268 | if nil != err { 269 | return nil, err 270 | } 271 | 272 | res := make([]*repository, len(content)) 273 | for i, elm := range content { 274 | n := elm.FName 275 | n = strings.TrimPrefix(n, prefix) 276 | n = strings.ReplaceAll(n, "/", string(AltPathSeparator)) 277 | r := &repository{ 278 | FName: n, 279 | FRemote: elm.FRemote, 280 | } 281 | r.Value = r 282 | r.Repository = emptyRepository 283 | r.keepdir = c.keepdir 284 | res[i] = r 285 | } 286 | 287 | return res, nil 288 | } 289 | 290 | func (c *gitlabClient) getRepositories(owner string, kind string) (res []*repository, err error) { 291 | defer trace(owner)(&err) 292 | 293 | var path string 294 | if "group" == kind { 295 | path = fmt.Sprintf("/groups/%s/projects?"+ 296 | "include_subgroups=true&simple=true&order_by=id&per_page=100", url.PathEscape(owner)) 297 | } else { 298 | path = fmt.Sprintf("/users/%s/projects?"+ 299 | "simple=true&order_by=id&per_page=100", url.PathEscape(owner)) 300 | } 301 | 302 | res = make([]*repository, 0) 303 | for page := 1; ; page++ { 304 | lst, err := c.getRepositoryPage(owner+"/", path+fmt.Sprintf("&page=%d", page)) 305 | if nil != err { 306 | return nil, err 307 | } 308 | res = append(res, lst...) 309 | if len(lst) < 100 { 310 | break 311 | } 312 | } 313 | 314 | return res, nil 315 | } 316 | -------------------------------------------------------------------------------- /src/prov/package_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * test.go 3 | * 4 | * Copyright 2021-2022 Bill Zissimopoulos 5 | */ 6 | /* 7 | * This file is part of Hubfs. 8 | * 9 | * You can redistribute it and/or modify it under the terms of the GNU 10 | * Affero General Public License version 3 as published by the Free 11 | * Software Foundation. 12 | */ 13 | 14 | package prov 15 | 16 | import ( 17 | "fmt" 18 | "os" 19 | "testing" 20 | 21 | libtrace "github.com/billziss-gh/golib/trace" 22 | ) 23 | 24 | var atinitFn []func() error 25 | var atexitFn []func() 26 | 27 | func atinit(fn func() error) { 28 | atinitFn = append(atinitFn, fn) 29 | } 30 | 31 | func atexit(fn func()) { 32 | atexitFn = append(atexitFn, fn) 33 | } 34 | 35 | func TestMain(m *testing.M) { 36 | libtrace.Verbose = true 37 | libtrace.Pattern = "github.com/winfsp/hubfs/*" 38 | 39 | for i := range atinitFn { 40 | err := atinitFn[i]() 41 | if nil != err { 42 | fmt.Printf("error: during init: %v\n", err) 43 | os.Exit(1) 44 | } 45 | } 46 | 47 | ec := m.Run() 48 | 49 | for i := range atexitFn { 50 | j := len(atexitFn) - 1 - i 51 | atexitFn[j]() 52 | } 53 | 54 | os.Exit(ec) 55 | } 56 | -------------------------------------------------------------------------------- /src/prov/provider.go: -------------------------------------------------------------------------------- 1 | /* 2 | * provider.go 3 | * 4 | * Copyright 2021-2022 Bill Zissimopoulos 5 | */ 6 | /* 7 | * This file is part of Hubfs. 8 | * 9 | * You can redistribute it and/or modify it under the terms of the GNU 10 | * Affero General Public License version 3 as published by the Free 11 | * Software Foundation. 12 | */ 13 | 14 | package prov 15 | 16 | import ( 17 | "errors" 18 | "io" 19 | "net/url" 20 | "sort" 21 | "sync" 22 | "time" 23 | 24 | libtrace "github.com/billziss-gh/golib/trace" 25 | ) 26 | 27 | type Provider interface { 28 | Auth() (string, error) 29 | NewClient(token string) (Client, error) 30 | } 31 | 32 | type Client interface { 33 | SetConfig(config []string) ([]string, error) 34 | GetDirectory() string 35 | GetOwners() ([]Owner, error) 36 | OpenOwner(name string) (Owner, error) 37 | CloseOwner(owner Owner) 38 | GetRepositories(owner Owner) ([]Repository, error) 39 | OpenRepository(owner Owner, name string) (Repository, error) 40 | CloseRepository(repository Repository) 41 | StartExpiration() 42 | StopExpiration() 43 | } 44 | 45 | type Owner interface { 46 | Name() string 47 | } 48 | 49 | type Repository interface { 50 | io.Closer 51 | GetDirectory() string 52 | SetDirectory(path string) error 53 | RemoveDirectory() error 54 | Name() string 55 | GetRefs() ([]Ref, error) 56 | GetRef(name string) (Ref, error) 57 | GetTempRef(name string) (Ref, error) 58 | GetTree(ref Ref, entry TreeEntry) ([]TreeEntry, error) 59 | GetTreeEntry(ref Ref, entry TreeEntry, name string) (TreeEntry, error) 60 | GetBlobReader(entry TreeEntry) (io.ReaderAt, error) 61 | GetModule(ref Ref, path string, rootrel bool) (string, error) 62 | } 63 | 64 | type Ref interface { 65 | Name() string 66 | Kind() RefKind 67 | TreeTime() time.Time 68 | } 69 | 70 | type TreeEntry interface { 71 | Name() string 72 | Mode() uint32 73 | Size() int64 74 | Target() string 75 | Hash() string 76 | } 77 | 78 | type RefKind int 79 | 80 | const ( 81 | RefTemp RefKind = iota 82 | RefBranch 83 | RefTag 84 | RefOther 85 | ) 86 | 87 | const AltPathSeparator = '+' 88 | 89 | var ErrNotFound = errors.New("not found") 90 | 91 | var regmutex sync.RWMutex 92 | var registry = make(map[string]func(uri *url.URL) Provider) 93 | var reghelp = make(map[string]string) 94 | 95 | func RegisterProviderClass(name string, ctor func(uri *url.URL) Provider, help string) { 96 | regmutex.Lock() 97 | defer regmutex.Unlock() 98 | registry[name] = ctor 99 | reghelp[name] = help 100 | } 101 | 102 | func GetProviderClassNames() (names []string) { 103 | regmutex.RLock() 104 | defer regmutex.RUnlock() 105 | names = make([]string, 0, len(registry)) 106 | for n := range registry { 107 | names = append(names, n) 108 | } 109 | sort.Strings(names) 110 | return 111 | } 112 | 113 | func GetProviderClassHelp(name string) string { 114 | regmutex.RLock() 115 | defer regmutex.RUnlock() 116 | return reghelp[name] 117 | } 118 | 119 | func GetProviderInstanceName(uri *url.URL) string { 120 | regmutex.RLock() 121 | defer regmutex.RUnlock() 122 | ctor := registry[uri.Host] 123 | if nil != ctor { 124 | return uri.Host 125 | } 126 | return uri.Scheme + "://" + uri.Host 127 | } 128 | 129 | func NewProviderInstance(uri *url.URL) Provider { 130 | regmutex.RLock() 131 | defer regmutex.RUnlock() 132 | ctor := registry[uri.Host] 133 | if nil != ctor { 134 | return ctor(uri) 135 | } 136 | ctor = registry[uri.Scheme+":"] 137 | if nil != ctor { 138 | return ctor(uri) 139 | } 140 | return nil 141 | } 142 | 143 | func trace(vals ...interface{}) func(vals ...interface{}) { 144 | return libtrace.Trace(1, "", vals...) 145 | } 146 | 147 | func tracef(form string, vals ...interface{}) { 148 | libtrace.Tracef(1, form, vals...) 149 | } 150 | -------------------------------------------------------------------------------- /src/pvt: -------------------------------------------------------------------------------- 1 | ../../hubfs.pvt -------------------------------------------------------------------------------- /src/pvt_ent.go: -------------------------------------------------------------------------------- 1 | //go:build ent 2 | // +build ent 3 | 4 | /* 5 | * pvt_ent.go 6 | * 7 | * Copyright 2021-2022 Bill Zissimopoulos 8 | */ 9 | /* 10 | * This file is part of Hubfs. 11 | * 12 | * You can redistribute it and/or modify it under the terms of the GNU 13 | * Affero General Public License version 3 as published by the Free 14 | * Software Foundation. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "github.com/winfsp/hubfs/pvt" 21 | ) 22 | 23 | func init() { 24 | pvt.Load() 25 | } 26 | -------------------------------------------------------------------------------- /src/util/event.go: -------------------------------------------------------------------------------- 1 | /* 2 | * event.go 3 | * 4 | * Copyright 2021-2022 Bill Zissimopoulos 5 | */ 6 | /* 7 | * This file is part of Hubfs. 8 | * 9 | * You can redistribute it and/or modify it under the terms of the GNU 10 | * Affero General Public License version 3 as published by the Free 11 | * Software Foundation. 12 | */ 13 | 14 | package util 15 | 16 | import "sync" 17 | 18 | var eventmux sync.RWMutex 19 | var eventmap = make(map[string][]func(interface{})) 20 | 21 | func RegisterEventHandler(name string, handler func(interface{})) { 22 | eventmux.Lock() 23 | defer eventmux.Unlock() 24 | eventmap[name] = append(eventmap[name], handler) 25 | } 26 | 27 | func InvokeEvent(name string, event interface{}) { 28 | eventmux.RLock() 29 | defer eventmux.RUnlock() 30 | for l, i := eventmap[name], 0; len(l) > i; { 31 | invokeEvent(l, &i, event) 32 | } 33 | } 34 | 35 | func invokeEvent(l []func(interface{}), i *int, event interface{}) { 36 | defer func() { 37 | recover() 38 | }() 39 | for len(l) > *i { 40 | j := *i 41 | (*i)++ 42 | l[j](event) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/util/event_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * event_test.go 3 | * 4 | * Copyright 2021-2022 Bill Zissimopoulos 5 | */ 6 | /* 7 | * This file is part of Hubfs. 8 | * 9 | * You can redistribute it and/or modify it under the terms of the GNU 10 | * Affero General Public License version 3 as published by the Free 11 | * Software Foundation. 12 | */ 13 | 14 | package util 15 | 16 | import ( 17 | "fmt" 18 | "testing" 19 | ) 20 | 21 | var testEventTotal1 int 22 | 23 | func testEventHandler1(event interface{}) { 24 | testEventTotal1 += event.(int) 25 | } 26 | 27 | var testEventTotal2 int 28 | 29 | func testEventHandler2(event interface{}) { 30 | testEventTotal2 -= event.(int) 31 | } 32 | 33 | func testEventPanic(event interface{}) { 34 | // should panic because event is of type int 35 | fmt.Println(event.(string)) 36 | } 37 | 38 | func TestEvent(t *testing.T) { 39 | RegisterEventHandler("test1", testEventHandler1) 40 | RegisterEventHandler("test1", testEventPanic) 41 | RegisterEventHandler("test1", testEventHandler1) 42 | 43 | RegisterEventHandler("test2", testEventHandler2) 44 | RegisterEventHandler("test2", testEventPanic) 45 | RegisterEventHandler("test2", testEventHandler2) 46 | 47 | testEventTotal1 = 0 48 | InvokeEvent("test1", 21) 49 | if testEventTotal1 != 42 { 50 | t.Error() 51 | } 52 | 53 | testEventTotal2 = 0 54 | InvokeEvent("test2", 21) 55 | if testEventTotal2 != -42 { 56 | t.Error() 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/util/optlist.go: -------------------------------------------------------------------------------- 1 | /* 2 | * optlist.go 3 | * 4 | * Copyright 2021-2022 Bill Zissimopoulos 5 | */ 6 | /* 7 | * This file is part of Hubfs. 8 | * 9 | * You can redistribute it and/or modify it under the terms of the GNU 10 | * Affero General Public License version 3 as published by the Free 11 | * Software Foundation. 12 | */ 13 | 14 | package util 15 | 16 | type Optlist []string 17 | 18 | // String implements flag.Value.String. 19 | func (l *Optlist) String() string { 20 | return "" 21 | } 22 | 23 | // Set implements flag.Value.Set. 24 | func (l *Optlist) Set(s string) error { 25 | *l = append(*l, s) 26 | return nil 27 | } 28 | --------------------------------------------------------------------------------