├── .gitignore
├── .idea
├── inspectionProfiles
│ └── profiles_settings.xml
├── misc.xml
├── modules.xml
├── sshfs-go.iml
├── vcs.xml
└── workspace.xml
├── .travis.yml
├── LICENSE
├── Makefile
├── README.md
├── cmd
├── docker.go
├── mount.go
├── root.go
├── utils.go
└── version.go
├── docker
├── config.go
├── driver.go
└── server.go
├── fs
├── dir.go
├── file.go
├── fs.go
├── node.go
├── ssh.go
└── utils.go
├── main.go
├── release
├── Dockerfile
└── build.sh
└── scripts
├── gofmtcheck.sh
└── golint.sh
/.gitignore:
--------------------------------------------------------------------------------
1 | ./idea/
2 | sshfs
3 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/sshfs-go.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/workspace.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | 123
60 | log.print
61 |
62 |
63 | $PROJECT_DIR$
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | ---
2 | language: go
3 |
4 | # we support the most two recent releases, and (hopefully) the tip.
5 | go:
6 | - 1.5
7 | - 1.6
8 | - tip
9 |
10 | cache:
11 | directories:
12 | - vendor
13 |
14 | install:
15 | - mkdir tools
16 | - curl -L https://github.com/Masterminds/glide/releases/download/0.9.3/glide-0.9.3-linux-amd64.tar.gz | tar -xzv --strip-components=1 -C tools
17 | - chmod +x tools/glide
18 | - export PATH=$(pwd)/tools:$PATH
19 | - glide install
20 |
21 | script:
22 | - make
23 |
24 | matrix:
25 | allow_failures:
26 | - go: tip
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | TEST?=$(shell GO15VENDOREXPERIMENT=1 go list -f '{{.ImportPath}}/...' ./... | grep -v /vendor/ | sed "s|$(shell go list -f '{{.ImportPath}}' .)|.|g" | sed "s/\.\/\.\.\./\.\//g")
2 | VET?=$(shell echo ${TEST} | sed "s/\.\.\.//g" | sed "s/\.\/ //g")
3 | VETARGS?=-asmdecl -atomic -bool -buildtags -copylocks -methods -nilfunc -printf -rangeloops -shift -structtags -unsafeptr
4 | NAME=$(shell awk -F\" '/^const Name/ { print $$2 }' cmd/version.go)
5 | VERSION=$(shell awk -F\" '/^const Version/ { print $$2 }' cmd/version.go)
6 |
7 | all: test sshfs
8 |
9 | sshfs:
10 | GO15VENDOREXPERIMENT=1 go build .
11 |
12 | install: fmtcheck
13 | GO15VENDOREXPERIMENT=1 go install .
14 |
15 | # test runs the unit tests and vets the code
16 | test: fmtcheck vet lint
17 | @echo "==> Testing"
18 | GO15VENDOREXPERIMENT=1 go test $(TEST) $(TESTARGS) -timeout=30s -parallel=4
19 |
20 | # testrace runs the race checker
21 | testrace: fmtcheck
22 | GO15VENDOREXPERIMENT=1 go test -race $(TEST) $(TESTARGS)
23 |
24 | cover:
25 | @go tool cover 2>/dev/null; if [ $$? -eq 3 ]; then \
26 | go get -u golang.org/x/tools/cmd/cover; \
27 | fi
28 | GO15VENDOREXPERIMENT=1 go test $(TEST) -coverprofile=coverage.out
29 | GO15VENDOREXPERIMENT=1 go tool cover -html=coverage.out
30 | rm coverage.out
31 |
32 | # vet runs the Go source code static analysis tool `vet` to find
33 | # any common errors.
34 | vet:
35 | @echo "==> Cheking that code complies with go vet requirements..."
36 | @go tool vet 2>/dev/null ; if [ $$? -eq 3 ]; then \
37 | go get golang.org/x/tools/cmd/vet; \
38 | fi
39 | @GO15VENDOREXPERIMENT=1 go tool vet $(VETARGS) $(VET) ; if [ $$? -eq 1 ]; then \
40 | echo ""; \
41 | echo "Vet found suspicious constructs. Please check the reported constructs"; \
42 | echo "and fix them if necessary before submitting the code for review."; \
43 | exit 1; \
44 | fi
45 |
46 | lint:
47 | @golint 2>/dev/null; if [ $$? -eq 127 ]; then \
48 | go get github.com/golang/lint/golint; \
49 | fi
50 | @GO15VENDOREXPERIMENT=1 sh -c "'$(CURDIR)/scripts/golint.sh' ${VET}"; if [ $$? -eq 1 ]; then \
51 | echo ""; \
52 | echo "lint found errors in the code. Please check the errors listed above"; \
53 | echo "and fix them if necessary before submitting the code for review."; \
54 | exit 1; \
55 | fi
56 |
57 | fmt:
58 | gofmt -w $(shell echo $(TEST) | sed 's/\.\.\./*.go/g' | sed 's/\.\//.\/*.go/')
59 |
60 | fmtcheck:
61 | @sh -c "'$(CURDIR)/scripts/gofmtcheck.sh'"
62 |
63 | sshfs-%-linux-amd64.tar.gz: test
64 | ./release/build.sh
65 | mkdir sshfs-$*-linux-amd64
66 | mv sshfs sshfs-$*-linux-amd64/sshfs
67 | cp README.md LICENSE sshfs-$*-linux-amd64/
68 | tar -czvf $@ sshfs-$*-linux-amd64
69 | rm -rf sshfs-$*-linux-amd64
70 |
71 | .PHONY: all test vet fmt fmtcheck lint
72 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SSHFS
2 |
3 | [](https://travis-ci.org/soopsio/sshfs)
4 |
5 | SSHFS mounts arbitrary [sftp](https://github.com/pkg/sftp) prefixes in a FUSE
6 | filesystem. It also provides a Docker volume plugin to the do the same for your
7 | containers.
8 |
9 |
10 | **Table of Contents**
11 |
12 | - [SSHFS](#sshfs)
13 | - [Mounting](#mounting)
14 | - [Docker](#docker)
15 | - [License](#license)
16 |
17 |
18 |
19 | # Installation
20 |
21 | This project is in early development and has not reached 1.0. You will have to
22 | build the binary yourself:
23 |
24 | ```shell
25 | go get github.com/soopsio/sshfs-go
26 | env GOOS=linux go build github.com/soopsio/sshfs-go
27 | ```
28 |
29 | # Usage
30 |
31 | SSHFS is one binary that can mount keys or run a Docker volume plugin to do so
32 | for containers. Run `sshfs --help` to see options not documented here.
33 |
34 | ## Mounting
35 |
36 | ```
37 | Usage:
38 | sshfs mount {mountpoint} [flags]
39 |
40 | Flags:
41 | -a, --address string ssh server address (default "127.0.0.1:22")
42 | -h, --help help for mount
43 | -p, --password string ssh password
44 | -r, --root string ssh root (default "/opt")
45 | -u, --username string ssh username (default "root")
46 | ```
47 |
48 | To mount secrets, first create a mountpoint (`mkdir test`), then use `sshfs`
49 | to mount:
50 |
51 | ```shell
52 | sshfs mount -a 10.10.10.10:22 -u root -p ****** --log-level debug -r /tmp/test /opt/data/tmp
53 | ```
54 |
55 | ## Docker
56 |
57 | ```
58 | Usage:
59 | sshfs docker {mountpoint} [flags]
60 |
61 | Flags:
62 | -a, --address string ssh server address (default "127.0.0.1:22")
63 | -h, --help help for docker
64 | -p, --password string ssh password
65 | -s, --socket string socket address to communicate with docker (default "/run/docker/plugins/ssh.sock")
66 | -u, --username string ssh username (default "root")
67 | ```
68 |
69 | To start the Docker plugin, create a directory to hold mountpoints (`mkdir
70 | test`), then use `sshfs` to start the server. When Docker volumes request a
71 | volume (`docker run --volume-driver vault --volume
72 | {prefix}:/container/secret/path`), the plugin will create mountpoints and manage
73 | FUSE servers automatically.
74 |
75 | ```shell
76 | sshfs docker /mnt/sshfs -a 10.10.10.10:22 -u root -p ****** --log-level debug -r /tmp/test
77 | ls /run/docker/plugins/
78 | ssh.sock
79 | docker run --rm -it -v myvola:/data --volume-driver=ssh alpine sh
80 | docker volume ls
81 | DRIVER VOLUME NAME
82 | ssh myvola
83 | ```
84 |
85 | # License
86 |
87 | SSHFS is licensed under an
88 | [Apache 2.0 License](http://www.apache.org/licenses/LICENSE-2.0.html) (see also:
89 | [LICENSE](LICENSE))
90 |
--------------------------------------------------------------------------------
/cmd/docker.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2016 Asteris, LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package cmd
16 |
17 | import (
18 | "errors"
19 | "os"
20 |
21 | "github.com/docker/go-plugins-helpers/volume"
22 | "github.com/sirupsen/logrus"
23 | "github.com/soopsio/sshfs-go/docker"
24 | "github.com/soopsio/sshfs-go/fs"
25 | "github.com/spf13/cobra"
26 | "github.com/spf13/viper"
27 | )
28 |
29 | // dockerCmd represents the docker command
30 | var dockerCmd = &cobra.Command{
31 | Use: "docker {mountpoint}",
32 | Short: "start the docker volume server at the specified root",
33 | PreRunE: func(cmd *cobra.Command, args []string) error {
34 | if len(args) == 0 {
35 | return errors.New("expected exactly one argument, a mountpoint")
36 | }
37 |
38 | if err := viper.BindPFlags(cmd.Flags()); err != nil {
39 | logrus.WithError(err).Fatal("could not bind flags")
40 | }
41 |
42 | return nil
43 | },
44 | Run: func(cmd *cobra.Command, args []string) {
45 | driver, err := docker.New(docker.Config{
46 | Root: viper.GetString("root"),
47 | MountPoint: args[0],
48 | SSHServer: viper.GetString("address"),
49 | SSHConfig: fs.NewConfig(viper.GetString("username"), viper.GetString("password"), viper.GetString("private-key")),
50 | })
51 | if err != nil {
52 | logrus.WithError(err).Fatal("driver init failed")
53 | }
54 |
55 | logrus.WithFields(logrus.Fields{
56 | "root": args[0],
57 | "address": viper.GetString("address"),
58 | "username": viper.GetString("username"),
59 | "socket": viper.GetString("socket"),
60 | }).Info("starting plugin server")
61 |
62 | defer func() {
63 | for _, err := range driver.Stop() {
64 | logrus.WithError(err).Error("error stopping driver")
65 | }
66 | }()
67 |
68 | handler := volume.NewHandler(driver)
69 | logrus.WithField("socket", viper.GetString("socket")).Info("serving unix socket")
70 | err = handler.ServeUnix(viper.GetString("socket"), 0)
71 | if err != nil {
72 | logrus.WithError(err).Fatal("failed serving")
73 | }
74 | },
75 | }
76 |
77 | func init() {
78 | RootCmd.AddCommand(dockerCmd)
79 |
80 | dockerCmd.Flags().StringP("address", "a", "127.0.0.1:22", "ssh server address")
81 | dockerCmd.Flags().StringP("username", "u", "root", "ssh username")
82 | dockerCmd.Flags().StringP("password", "p", "", "ssh password")
83 | dockerCmd.Flags().StringP("root", "r", "/tmp", "remote root")
84 | dockerCmd.Flags().StringP("private-key", "i", os.Getenv("HOME")+`/.ssh/id_rsa`, "path to private ssh key")
85 | dockerCmd.Flags().StringP("socket", "s", "/run/docker/plugins/ssh.sock", "socket address to communicate with docker")
86 | }
87 |
--------------------------------------------------------------------------------
/cmd/mount.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2016 Asteris, LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package cmd
16 |
17 | import (
18 | "errors"
19 | "github.com/sirupsen/logrus"
20 | "github.com/soopsio/sshfs-go/fs"
21 | "github.com/spf13/cobra"
22 | "github.com/spf13/viper"
23 | "os"
24 | "os/signal"
25 | "syscall"
26 | )
27 |
28 | // mountCmd represents the mount command
29 | var mountCmd = &cobra.Command{
30 | Use: "mount {mountpoint}",
31 | Short: "mount a SSHFS at the specified mountpoint",
32 | PreRunE: func(cmd *cobra.Command, args []string) error {
33 | if len(args) == 0 {
34 | return errors.New("expected exactly one argument")
35 | }
36 |
37 | if err := viper.BindPFlags(cmd.Flags()); err != nil {
38 | logrus.WithError(err).Fatal("could not bind flags")
39 | }
40 |
41 | return nil
42 | },
43 | Run: func(cmd *cobra.Command, args []string) {
44 | config := fs.NewConfig(viper.GetString("username"), viper.GetString("password"), viper.GetString("private-key"))
45 | logrus.WithField("address", viper.GetString("address")).Info("creating FUSE client for SSH Server")
46 |
47 | fs, err := fs.New(config, args[0], viper.GetString("address"), viper.GetString("root"))
48 | if err != nil {
49 | logrus.WithError(err).Fatal("error creatinging fs")
50 | }
51 |
52 | // handle interrupt
53 | go func() {
54 | c := make(chan os.Signal, 1)
55 | signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
56 |
57 | <-c
58 | logrus.Info("stopping")
59 | err := fs.Unmount()
60 | if err != nil {
61 | logrus.WithError(err).Fatal("could not unmount cleanly")
62 | }
63 | }()
64 |
65 | err = fs.Mount()
66 | if err != nil {
67 | logrus.WithError(err).Fatal("could not continue")
68 | }
69 | },
70 | }
71 |
72 | func init() {
73 | RootCmd.AddCommand(mountCmd)
74 |
75 | mountCmd.Flags().StringP("address", "a", "127.0.0.1:22", "ssh server address")
76 | mountCmd.Flags().StringP("username", "u", "root", "ssh username")
77 | mountCmd.Flags().StringP("password", "p", "", "ssh password")
78 | mountCmd.Flags().StringP("root", "r", "/opt", "ssh root")
79 | mountCmd.Flags().StringP("private-key", "i", os.Getenv("HOME")+`/.ssh/id_rsa`, "path to private ssh key")
80 | }
81 |
--------------------------------------------------------------------------------
/cmd/root.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2016 Asteris, LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package cmd
16 |
17 | import (
18 | "os"
19 |
20 | "github.com/sirupsen/logrus"
21 | "github.com/spf13/cobra"
22 | "github.com/spf13/viper"
23 | )
24 |
25 | var cfgFile string
26 |
27 | // RootCmd controls global settings
28 | var RootCmd = &cobra.Command{
29 | Use: "sshfs",
30 | Short: "use Docker's volumes to mount sshfs",
31 | Long: `use Docker's volumes to mount sshfs`,
32 | }
33 |
34 | // Execute adds all child commands to the root command sets flags appropriately.
35 | // This is called by main.main(). It only needs to happen once to the rootCmd.
36 | func Execute() {
37 | if err := RootCmd.Execute(); err != nil {
38 | logrus.WithError(err).Error("error executing command")
39 | os.Exit(-1)
40 | }
41 | }
42 |
43 | func init() {
44 | cobra.OnInitialize(initConfig, initLogging, lockMemory)
45 |
46 | RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is /etc/sysconfig/sshfs)")
47 |
48 | // logging flags
49 | RootCmd.PersistentFlags().String("log-level", "info", "log level (one of fatal, error, warn, info, or debug)")
50 | RootCmd.PersistentFlags().String("log-format", "text", "log level (one of text or json)")
51 | RootCmd.PersistentFlags().String("log-destination", "stdout:", "log destination (file:/your/output, stdout:, journald:, or syslog://tag@host:port#protocol)")
52 |
53 | if err := viper.BindPFlags(RootCmd.PersistentFlags()); err != nil {
54 | logrus.WithError(err).Fatal("could not bind flags")
55 | }
56 | }
57 |
58 | // initConfig reads in config file and ENV variables if set.
59 | func initConfig() {
60 | if cfgFile != "" { // enable ability to specify config file via flag
61 | viper.SetConfigFile(cfgFile)
62 | }
63 |
64 | viper.SetConfigName("sshfs") // name of config file (without extension)
65 | viper.AddConfigPath("/etc/sysconfig") // adding sysconfig as the first search path
66 | viper.AddConfigPath("$HOME") // home directory as another path
67 | viper.AutomaticEnv() // read in environment variables that match
68 |
69 | // If a config file is found, read it in.
70 | if err := viper.ReadInConfig(); err == nil {
71 | logrus.WithField("config", viper.ConfigFileUsed()).Info("using config file from disk")
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/cmd/utils.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2016 Asteris, LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package cmd
16 |
17 | import (
18 | "log/syslog"
19 | "net/url"
20 |
21 | "github.com/rifflock/lfshook"
22 | "github.com/sirupsen/logrus"
23 | logrus_syslog "github.com/sirupsen/logrus/hooks/syslog"
24 | "github.com/spf13/viper"
25 | "github.com/wercker/journalhook"
26 | "golang.org/x/sys/unix"
27 | )
28 |
29 | func initLogging() {
30 | // level
31 | level, err := logrus.ParseLevel(viper.GetString("log-level"))
32 | if err != nil {
33 | logrus.WithError(err).Warn(`invalid log level. Defaulting to "info"`)
34 | level = logrus.InfoLevel
35 | }
36 | logrus.SetLevel(level)
37 |
38 | // format
39 | switch viper.GetString("log-format") {
40 | case "text":
41 | logrus.SetFormatter(new(logrus.TextFormatter))
42 | case "json":
43 | logrus.SetFormatter(new(logrus.JSONFormatter))
44 | default:
45 | logrus.SetFormatter(new(logrus.TextFormatter))
46 | logrus.WithField("format", viper.GetString("log-format")).Warn(`invalid log format. Defaulting to "text"`)
47 | }
48 |
49 | // output
50 | dest, err := url.Parse(viper.GetString("log-destination"))
51 | if err != nil {
52 | logrus.WithError(err).WithField("destination", viper.GetString("log-destination")).Error(`invalid log destination. Defaulting to "stdout:"`)
53 | dest.Scheme = "stdout"
54 | }
55 |
56 | switch dest.Scheme {
57 | case "stdout":
58 | // default, we don't need to do anything
59 | case "file":
60 | logrus.AddHook(lfshook.NewHook(lfshook.PathMap{
61 | logrus.DebugLevel: dest.Opaque,
62 | logrus.InfoLevel: dest.Opaque,
63 | logrus.WarnLevel: dest.Opaque,
64 | logrus.ErrorLevel: dest.Opaque,
65 | logrus.FatalLevel: dest.Opaque,
66 | }, &logrus.JSONFormatter{}))
67 | case "journald":
68 | journalhook.Enable()
69 | case "syslog":
70 | hook, err := logrus_syslog.NewSyslogHook(dest.Fragment, dest.Host, syslog.LOG_DEBUG, dest.User.String())
71 | if err != nil {
72 | logrus.WithError(err).Error("could not configure syslog hook")
73 | } else {
74 | logrus.AddHook(hook)
75 | }
76 | default:
77 | logrus.WithField("destination", viper.GetString("log-destination")).Warn(`invalid log destination. Defaulting to "stdout:"`)
78 | }
79 | }
80 |
81 | func lockMemory() {
82 | err := unix.Mlockall(unix.MCL_FUTURE | unix.MCL_CURRENT)
83 | switch err {
84 | case nil:
85 | case unix.ENOSYS:
86 | logrus.WithError(err).Warn("mlockall() not implemented on this system")
87 | case unix.ENOMEM:
88 | logrus.WithError(err).Warn("mlockall() failed with ENOMEM")
89 | default:
90 | logrus.WithError(err).Warn("could not perform mlockall to prevent swapping memory")
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/cmd/version.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2016 Asteris
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package cmd
16 |
17 | import (
18 | "fmt"
19 |
20 | "github.com/spf13/cobra"
21 | )
22 |
23 | // Name describes the name of this tool
24 | const Name = "sshfs"
25 |
26 | // Version describes the version of this tool
27 | const Version = "1.0.0"
28 |
29 | // versionCmd represents the version command
30 | var versionCmd = &cobra.Command{
31 | Use: "version",
32 | Short: "A brief description of your command",
33 | Run: func(cmd *cobra.Command, args []string) {
34 | fmt.Printf("%s %s", Name, Version)
35 | },
36 | }
37 |
38 | func init() {
39 | RootCmd.AddCommand(versionCmd)
40 | }
41 |
--------------------------------------------------------------------------------
/docker/config.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2016 Asteris, LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package docker
16 |
17 | import (
18 | "golang.org/x/crypto/ssh"
19 | )
20 |
21 | // Config configures the docker volume plugin
22 | type Config struct {
23 | // Root for mount
24 | Root string
25 | MountPoint string
26 | // Address and config for ssh
27 | SSHServer string
28 | SSHConfig *ssh.ClientConfig
29 | }
30 |
--------------------------------------------------------------------------------
/docker/driver.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2016 Asteris, LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package docker
16 |
17 | import (
18 | "fmt"
19 | "github.com/getlantern/errors"
20 | "github.com/pkg/sftp"
21 | "github.com/soopsio/sshfs-go/fs"
22 | "log"
23 | "net/url"
24 | "os"
25 | "path"
26 | "path/filepath"
27 | "sync"
28 |
29 | "github.com/docker/go-plugins-helpers/volume"
30 | "github.com/sirupsen/logrus"
31 | )
32 |
33 | type volumeName struct {
34 | name string
35 | connections int
36 | }
37 |
38 | // Driver implements the interface for a Docker volume plugin
39 | type Driver struct {
40 | config Config
41 | servers map[string]*Server
42 | volumes map[string]*volumeName
43 | sftp *sftp.Client
44 | m *sync.Mutex
45 | }
46 |
47 | // New instantiates a new driver and returns it
48 | func New(config Config) (*Driver, error) {
49 | client, err := fs.NewSftp(config.SSHConfig, config.SSHServer)
50 | if err != nil {
51 | return nil, err
52 | }
53 | return &Driver{
54 | sftp: client,
55 | config: config,
56 | servers: map[string]*Server{},
57 | m: new(sync.Mutex),
58 | }, nil
59 | }
60 |
61 | // Create handles volume creation calls
62 | func (d *Driver) Create(r *volume.CreateRequest) error {
63 | log.Println("Create Volume:")
64 | remotePath := filepath.Join(d.config.Root, r.Name)
65 | stat, err := d.sftp.Stat(remotePath)
66 | //log.Println(remotePath, stat, err)
67 | if err != nil {
68 | if err == os.ErrNotExist {
69 | err = d.sftp.Mkdir(remotePath)
70 | //log.Println(remotePath, stat, err)
71 | if err != nil {
72 | return err
73 | }
74 | stat, _ = d.sftp.Stat(remotePath)
75 | //log.Println(remotePath, stat, err)
76 | } else {
77 | return err
78 | }
79 | }
80 | if !stat.IsDir() {
81 | return errors.New(remotePath + " not direcory!")
82 | }
83 |
84 | if err := os.MkdirAll(d.mountpoint(r.Name), os.ModeDir); err != nil {
85 | return err
86 | }
87 |
88 | if d.volumes == nil {
89 | d.volumes = map[string]*volumeName{}
90 | }
91 | d.volumes[d.mountpoint(r.Name)] = &volumeName{name: r.Name}
92 | return nil
93 | }
94 |
95 | // Get retrieves a volume
96 | func (d *Driver) Get(r *volume.GetRequest) (*volume.GetResponse, error) {
97 | log.Println("Get Volume:", r)
98 | d.m.Lock()
99 | defer d.m.Unlock()
100 | m := d.mountpoint(r.Name)
101 | if s, ok := d.volumes[m]; ok {
102 | return &volume.GetResponse{Volume: &volume.Volume{Name: s.name, Mountpoint: d.mountpoint(s.name)}}, nil
103 | }
104 |
105 | return &volume.GetResponse{}, fmt.Errorf("Unable to find volume mounted on %s", m)
106 | }
107 |
108 | // List mounted volumes
109 | func (d *Driver) List() (*volume.ListResponse, error) {
110 | log.Println("List Volume")
111 | d.m.Lock()
112 | defer d.m.Unlock()
113 | var vols []*volume.Volume
114 | for _, v := range d.volumes {
115 | vols = append(vols, &volume.Volume{Name: v.name, Mountpoint: d.mountpoint(v.name)})
116 | }
117 | return &volume.ListResponse{Volumes: vols}, nil
118 | }
119 |
120 | // Remove handles volume removal calls
121 | func (d *Driver) Remove(r *volume.RemoveRequest) error {
122 | d.m.Lock()
123 | defer d.m.Unlock()
124 | mount := d.mountpoint(r.Name)
125 | logger := logrus.WithFields(logrus.Fields{
126 | "name": r.Name,
127 | "mountpoint": mount,
128 | })
129 | logger.Debug("got remove request")
130 |
131 | if server, ok := d.servers[mount]; ok {
132 | if server.connections <= 1 {
133 | logger.Debug("removing server")
134 | delete(d.servers, mount)
135 | }
136 | }
137 |
138 | log.Println(d.servers)
139 | return nil
140 | }
141 |
142 | // Path handles calls for mountpoints
143 | func (d *Driver) Path(r *volume.PathRequest) (*volume.PathResponse, error) {
144 | return &volume.PathResponse{Mountpoint: d.mountpoint(r.Name)}, nil
145 | }
146 |
147 | // Mount handles creating and mounting servers
148 | func (d *Driver) Mount(r *volume.MountRequest) (*volume.MountResponse, error) {
149 | d.m.Lock()
150 | defer d.m.Unlock()
151 |
152 | mount := d.mountpoint(r.Name)
153 | logger := logrus.WithFields(logrus.Fields{
154 | "name": r.Name,
155 | "mountpoint": mount,
156 | })
157 | logger.Info("mounting volume")
158 |
159 | server, ok := d.servers[mount]
160 | if ok && server.connections > 0 {
161 | server.connections++
162 | return &volume.MountResponse{Mountpoint: mount}, nil
163 | }
164 |
165 | mountInfo, err := os.Lstat(mount)
166 |
167 | if os.IsNotExist(err) {
168 | if err := os.MkdirAll(mount, os.ModeDir|0444); err != nil {
169 | logger.WithError(err).Error("error making mount directory")
170 | return &volume.MountResponse{}, err
171 | }
172 | } else if err != nil {
173 | logger.WithError(err).Error("error checking if directory exists")
174 | return &volume.MountResponse{}, err
175 | }
176 |
177 | if mountInfo != nil && !mountInfo.IsDir() {
178 | logger.Error("already exists and not a directory")
179 | return &volume.MountResponse{}, fmt.Errorf("%s already exists and is not a directory", mount)
180 | }
181 |
182 | server, err = NewServer(d.config.SSHConfig, mount, d.config.SSHServer, filepath.Join(d.config.Root, r.Name))
183 | if err != nil {
184 | logger.WithError(err).Error("error creating server")
185 | return &volume.MountResponse{}, err
186 | }
187 |
188 | go server.Mount()
189 | d.servers[mount] = server
190 |
191 | return &volume.MountResponse{Mountpoint: mount}, nil
192 | }
193 |
194 | // Unmount handles unmounting (but not removing) servers
195 | func (d *Driver) Unmount(r *volume.UnmountRequest) error {
196 | d.m.Lock()
197 | defer d.m.Unlock()
198 |
199 | mount := d.mountpoint(r.Name)
200 | logger := logrus.WithFields(logrus.Fields{
201 | "name": r.Name,
202 | "mountpoint": mount,
203 | })
204 | logger.Info("unmounting volume")
205 |
206 | if server, ok := d.servers[mount]; ok {
207 | logger.WithField("conns", server.connections).Debug("found server")
208 | if server.connections == 1 {
209 | logger.Debug("unmounting")
210 | err := server.Unmount()
211 | if err != nil {
212 | logger.WithError(err).Error("error unmounting server")
213 | return err
214 | }
215 | server.connections--
216 | }
217 | } else {
218 | logger.Error("could not find volume")
219 | return fmt.Errorf("unable to find the volume mounted at %s", mount)
220 | }
221 |
222 | d.sftp.RemoveDirectory(filepath.Join(d.config.Root, r.Name))
223 | d.sftp.Close()
224 | return nil
225 | }
226 |
227 | func (d *Driver) mountpoint(name string) string {
228 | return path.Join(d.config.MountPoint, url.QueryEscape(name))
229 | }
230 |
231 | // Capabilities Driver
232 | func (d *Driver) Capabilities() *volume.CapabilitiesResponse {
233 | return &volume.CapabilitiesResponse{Capabilities: volume.Capability{Scope: "local"}}
234 | }
235 |
236 | // Stop stops all the servers
237 | func (d *Driver) Stop() []error {
238 | d.m.Lock()
239 | defer d.m.Unlock()
240 | logrus.Debug("got stop request")
241 |
242 | errs := []error{}
243 | for _, server := range d.servers {
244 | err := server.Unmount()
245 | if err != nil {
246 | errs = append(errs, err)
247 | }
248 | }
249 |
250 | return errs
251 | }
252 |
--------------------------------------------------------------------------------
/docker/server.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2016 Asteris, LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package docker
16 |
17 | import (
18 | "github.com/sirupsen/logrus"
19 | "github.com/soopsio/sshfs-go/fs"
20 | "golang.org/x/crypto/ssh"
21 | )
22 |
23 | // Server wraps SSHFS and tracks connection counts
24 | type Server struct {
25 | fs *fs.SSHFS
26 | connections int
27 | stopFunc func()
28 | errs chan error
29 | }
30 |
31 | // NewServer returns a new server with initial state
32 | func NewServer(config *ssh.ClientConfig, mountpoint, server, root string) (*Server, error) {
33 | fs, err := fs.New(config, mountpoint, server, root)
34 | if err != nil {
35 | return nil, err
36 | }
37 |
38 | return &Server{fs: fs, connections: 1}, nil
39 | }
40 |
41 | // Mount mounts the wrapped FS on a given mountpoint. It also starts watching
42 | // for errors, which it will log.
43 | func (s *Server) Mount() error {
44 | err := s.fs.Mount()
45 |
46 | if err != nil {
47 | logrus.WithError(err).Error("error in server, stopping")
48 | return err
49 | }
50 |
51 | return nil
52 | }
53 |
54 | // Unmount stops the wrapped FS. It returns the last error that it sees, but
55 | // will log any others it receives.
56 | func (s *Server) Unmount() error {
57 | err := s.fs.Unmount()
58 |
59 | if err != nil {
60 | logrus.WithError(err).Error("could not unmount cleanly")
61 | }
62 |
63 | return err
64 | }
65 |
--------------------------------------------------------------------------------
/fs/dir.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2016 Asteris, LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package fs
16 |
17 | import (
18 | "bazil.org/fuse"
19 | "bazil.org/fuse/fs"
20 | "hash/crc64"
21 | "log"
22 | "os"
23 | "path"
24 | "path/filepath"
25 | "sync"
26 | "syscall"
27 | "time"
28 |
29 | //krfs "github.com/kr/fs"
30 | "github.com/pkg/sftp"
31 | "github.com/sirupsen/logrus"
32 | "golang.org/x/net/context"
33 | )
34 |
35 | var table = crc64.MakeTable(crc64.ISO)
36 |
37 | // Dir implements both Node and Handle
38 | type Dir struct {
39 | *Node
40 | Files *[]*File
41 | Dirs *[]*Dir
42 | sync.Mutex
43 | }
44 |
45 | var _ fs.Node = (*Dir)(nil)
46 |
47 | // NewRoot creates a new root and returns it
48 | func NewRoot(root string, c *sftp.Client) *Node {
49 | rnode := NewNode(c, 0, nil, root, true, true)
50 | return rnode
51 | }
52 |
53 | var _ fs.NodeOpener = (*Dir)(nil)
54 |
55 | // Open Dir
56 | func (d *Dir) Open(ctx context.Context, req *fuse.OpenRequest, resp *fuse.OpenResponse) (fs.Handle, error) {
57 | logrus.Debug("handling Dir.Open call")
58 | fh := &Dir{
59 | Node: d.Node,
60 | }
61 |
62 | fh.Lock()
63 | return fh, nil
64 | }
65 |
66 | var _ fs.NodeSetattrer = (*Dir)(nil)
67 |
68 | // Setattr Dir
69 | func (d *Dir) Setattr(ctx context.Context, req *fuse.SetattrRequest, resp *fuse.SetattrResponse) error {
70 | logrus.WithField("req", req).Debug("handling Dir.Setattr call")
71 | return nil
72 | }
73 |
74 | var _ fs.HandleReleaser = (*Dir)(nil)
75 |
76 | // Release Dir
77 | func (d *Dir) Release(ctx context.Context, req *fuse.ReleaseRequest) error {
78 | logrus.Debug("handling Dir.Release call", d.Path())
79 | d.Unlock()
80 | return nil
81 | }
82 |
83 | // Attr sets attrs on the given fuse.Attr
84 | func (d *Dir) Attr(ctx context.Context, a *fuse.Attr) error {
85 | logrus.WithField("path", d.Path()).Debug("handling Dir.Attr call")
86 | stat, err := d.sftp.Stat(d.Path())
87 | if err != nil {
88 | return err
89 | }
90 |
91 | statT, ok := stat.Sys().(*sftp.FileStat)
92 | if ok {
93 | a.Atime = time.Unix(int64(statT.Atime), 0)
94 | }
95 |
96 | a.Inode = d.GetInode()
97 | a.Mode = stat.Mode()
98 | a.Mtime = stat.ModTime()
99 | a.Ctime = stat.ModTime()
100 | a.Size = 4096 // linux 文件系统目录的固定大小,每创建一个文件将分配 4096 字节
101 | return nil
102 | }
103 |
104 | var _ fs.NodeStringLookuper = (*Dir)(nil)
105 |
106 | // Lookup looks up a path
107 | func (d *Dir) Lookup(ctx context.Context, name string) (fs.Node, error) {
108 | logrus.WithField("name", name).Debug("handling Dir.Lookup call")
109 | //time.Sleep(10 * time.Second)
110 | path := path.Join(d.Path(), name)
111 |
112 | childNode, ok := d.Node.GetChild(name)
113 | if ok {
114 | if childNode.isdir {
115 | return childNode.Dir, nil
116 | }
117 | return childNode.File, nil
118 | }
119 |
120 | // 本地缓存找不到对象则检查远程是否存在并添加到本地缓存
121 | f, err := d.sftp.Stat(path)
122 | logrus.Debugln("oooooo ", path, " ", err.Error())
123 | if err != nil {
124 | if err == os.ErrNotExist {
125 | return nil, fuse.ENOENT
126 | }
127 | return nil, err
128 | }
129 | // 本地没有,远程有时,本地创建节点
130 | childnode := NewNode(d.sftp, 0, d.Node, f.Name(), f.IsDir(), false)
131 |
132 | if f.IsDir() {
133 | directories := []*Dir{childnode.Dir}
134 | if d.Dirs != nil {
135 | directories = append(*d.Dirs, directories...)
136 | }
137 | d.Dirs = &directories
138 | return childnode.Dir, nil
139 | }
140 | files := []*File{childnode.File}
141 | if d.Files != nil {
142 | files = append(*d.Files, files...)
143 | }
144 | d.Files = &files
145 | return childnode.File, nil
146 | }
147 |
148 | var _ fs.NodeRemover = (*Dir)(nil)
149 |
150 | // Remove Dir
151 | func (d *Dir) Remove(ctx context.Context, req *fuse.RemoveRequest) error {
152 | logrus.WithField("current", d.Path()).WithField("req", req).Debug("handling Root.Remove call")
153 | path := filepath.Join(d.Path(), req.Name)
154 | rmnode, _ := d.GetChild(req.Name)
155 |
156 | if req.Dir {
157 | if rmnode.Dir.Dirs != nil {
158 | if len(*rmnode.Dirs) > 0 {
159 | return fuse.Errno(syscall.ENOTEMPTY)
160 | }
161 | }
162 | if rmnode.Dir.Files != nil {
163 | if len(*rmnode.Files) > 0 {
164 | return fuse.Errno(syscall.ENOTEMPTY)
165 | }
166 | }
167 |
168 | if err := d.sftp.RemoveDirectory(path); err != nil {
169 | return err
170 | }
171 | if rmnode != nil {
172 | rmnode.Remove()
173 | }
174 | newDirs := []*Dir{}
175 | for _, directory := range *d.Dirs {
176 | if directory.name != req.Name {
177 | newDirs = append(newDirs, directory)
178 | } else {
179 | if directory.Files != nil {
180 | return fuse.Errno(syscall.ENOTEMPTY)
181 | }
182 | }
183 | }
184 | d.Dirs = &newDirs
185 | } else {
186 | if err := d.sftp.Remove(path); err != nil {
187 | return err
188 | }
189 |
190 | if rmnode != nil {
191 | rmnode.Remove()
192 | }
193 |
194 | if d.Files != nil {
195 | newFiles := []*File{}
196 | for _, file := range *d.Files {
197 | if file.name != req.Name {
198 | newFiles = append(newFiles, file)
199 | }
200 | }
201 | d.Files = &newFiles
202 | }
203 | }
204 |
205 | return nil
206 | }
207 |
208 | var _ fs.HandleReadDirAller = (*Dir)(nil)
209 |
210 | // ReadDirAll returns a list of sshfs
211 | func (d *Dir) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) {
212 | logrus.WithField("dir", d).Debug("handling Dir.ReadDirAll call")
213 | //log.Println(d.name, d.path, d.isroot, d.Path())
214 | //d.Lock()
215 | //defer d.Lock()
216 | dirs := []fuse.Dirent{}
217 | fs, err := d.sftp.ReadDir(path.Join(d.Path()))
218 | if err != nil {
219 | return dirs, err
220 | }
221 |
222 | directories := []*Dir{}
223 | files := []*File{}
224 |
225 | for _, f := range fs {
226 | t := fuse.DT_File
227 | childnode, ok := d.Node.GetChild(f.Name())
228 | if !ok {
229 | childnode = NewNode(d.sftp, 0, d.Node, f.Name(), f.IsDir(), false)
230 | }
231 | if f.IsDir() {
232 | t = fuse.DT_Dir
233 | directories = append(directories, childnode.Dir)
234 | } else {
235 | files = append(files, childnode.File)
236 | }
237 |
238 | d := fuse.Dirent{
239 | Name: childnode.name,
240 | Inode: childnode.inode,
241 | Type: t,
242 | }
243 | dirs = append(dirs, d)
244 | }
245 | d.Node.Dir.Dirs = &directories
246 | d.Node.Dir.Files = &files
247 | return dirs, nil
248 | }
249 |
250 | var _ fs.NodeMkdirer = (*Dir)(nil)
251 |
252 | // Mkdir Dir
253 | func (d *Dir) Mkdir(ctx context.Context, req *fuse.MkdirRequest) (fs.Node, error) {
254 | logrus.Debug("handling Dir.Mkdir call")
255 | childnode, ok := d.GetChild(req.Name)
256 | if ok {
257 | if childnode.isdir {
258 | return childnode.Dir, nil
259 | }
260 | return childnode.File, nil
261 | }
262 |
263 | newNode := NewNode(d.sftp, 0, d.Node, req.Name, true, false)
264 |
265 | err := d.sftp.Mkdir(newNode.Path())
266 | if err != nil {
267 | return nil, err
268 | }
269 |
270 | err = d.sftp.Chmod(newNode.Path(), req.Mode)
271 | if err != nil {
272 | return nil, err
273 | }
274 |
275 | err = d.sftp.Chown(newNode.Path(), int(req.Uid), int(req.Gid))
276 | if err != nil {
277 | return nil, err
278 | }
279 |
280 | dirs := []*Dir{newNode.Dir}
281 | if d.Dirs != nil {
282 | dirs = append(*d.Dirs, dirs...)
283 | }
284 | d.Dirs = &dirs
285 |
286 | return newNode.Dir, nil
287 | }
288 |
289 | var _ fs.NodeCreater = (*Dir)(nil)
290 |
291 | // Create Dir
292 | func (d *Dir) Create(ctx context.Context, req *fuse.CreateRequest, resp *fuse.CreateResponse) (fs.Node, fs.Handle, error) {
293 | logrus.Debug("handling Dir.Create call")
294 | node, ok := d.GetChild(req.Name)
295 | if ok {
296 | return node.File, node.File, nil
297 | }
298 |
299 | newNode := NewNode(d.sftp, 0, d.Node, req.Name, false, false)
300 |
301 | file, err := d.sftp.Create(newNode.Path())
302 | if err != nil {
303 | return nil, nil, err
304 | }
305 |
306 | err = d.sftp.Chmod(newNode.Path(), req.Mode)
307 | if err != nil {
308 | return nil, nil, err
309 | }
310 |
311 | err = d.sftp.Chown(newNode.Path(), int(req.Uid), int(req.Gid))
312 | if err != nil {
313 | return nil, nil, err
314 | }
315 |
316 | files := []*File{newNode.File}
317 | if d.Files != nil {
318 | files = append(*d.Files, files...)
319 | }
320 | d.Files = &files
321 | newNode.File.file = file
322 | newNode.File.Lock()
323 | return newNode.File, newNode.File, nil
324 | }
325 |
326 | // Rename Dir
327 | func (d *Dir) Rename(ctx context.Context, req *fuse.RenameRequest, newDir fs.Node) error {
328 | log.Println("Rename requested from", req.OldName, "to", req.NewName)
329 | newParentNode := newDir.(*Dir).Node
330 | opath := filepath.Join(d.Path(), req.OldName)
331 | npath := filepath.Join(newParentNode.Path(), req.NewName)
332 | d.Lock()
333 | defer d.Unlock()
334 |
335 | // Rename 不改变 iNode
336 | onode, _ := d.GetChild(req.OldName)
337 | //log.Printf("OldParent: %s, OldName: %s, NewParent: %s, NewnName: %s", d.name, req.OldName, newParentNode.name, req.NewName)
338 | // # tree --inodes tmp
339 | //tmp/
340 | //|-- [8632662105] dira
341 | //| |-- [8632662109] d1
342 | //| | `-- [8632662111] a1
343 | //| |-- [8632662110] d2
344 | //| `-- [8632662108] t
345 | //`-- [8632662106] dirb
346 | // test 为 RootName
347 | // 1. 同目录,不同名
348 | // # mv dira/t dira/t1
349 | // OldParent: dira, OldName: t, NewParent: dira, NewnName: t1
350 | // 2. 同名不同目录
351 | // mv tmp/dira/t tmp/dirb/t
352 | // OldParent: dira, OldName: t, NewParent: dirb, NewnName: t
353 | // 3. 文件不同名不同目录
354 | // mv tmp/dira/t tmp/dirb/t1
355 | // OldParent: dira, OldName: t, NewParent: dirb, NewnName: t1
356 | // 4. 非空目录不同目录同名
357 | // # mv tmp/dira tmp/dirb/
358 | // OldParent: test, OldName: dira, NewParent: dirb, NewnName: dira
359 | // 5. 非空目录同目录不同名
360 | // # mv tmp/dira tmp/dirc
361 | // OldParent: test, OldName: dira, NewParent: test, NewnName: dirc
362 |
363 | // onode 为当前要 rename 的对象节点(目录或文件),当前目录为 d.Node 为 onode.parent
364 | // newParentNode 新对象节点的父节点
365 |
366 | // 变更 Node 信息
367 | d.Node.Rename(onode, newParentNode, req.NewName)
368 | d.sftp.Rename(opath, npath)
369 |
370 | if newParentNode.inode == d.Node.inode {
371 | return nil
372 | }
373 |
374 | // 移动到新目录
375 | if onode.isdir {
376 | // 清除旧父节点记录
377 | if d.Dirs != nil {
378 | directories := []*Dir{}
379 | for _, dir := range *d.Dirs {
380 | if dir.inode != onode.inode {
381 | directories = append(directories, dir)
382 | }
383 | }
384 | d.Dirs = &directories
385 | }
386 |
387 | //新的父节点增加记录
388 | if newParentNode.Dirs != nil {
389 | directories := []*Dir{onode.Dir}
390 | for _, dir := range *d.Dirs {
391 | if dir.inode != onode.inode {
392 | directories = append(directories, dir)
393 | }
394 | }
395 | d.Dirs = &directories
396 | }
397 |
398 | return nil
399 | }
400 | // 清除旧父节点记录
401 | if d.Files != nil {
402 | files := []*File{}
403 | for _, file := range *d.Files {
404 | if file.inode != onode.inode {
405 | files = append(files, file)
406 | }
407 | }
408 | d.Files = &files
409 | }
410 |
411 | //新的父节点增加记录
412 | if newParentNode.Files != nil {
413 | files := []*File{onode.File}
414 | for _, file := range *d.Files {
415 | if file.inode != onode.inode {
416 | files = append(files, file)
417 | }
418 | }
419 | d.Files = &files
420 | }
421 |
422 | return nil
423 | }
424 |
425 | var _ fs.NodeSymlinker = (*Dir)(nil)
426 |
427 | // Symlink Dir
428 | func (d *Dir) Symlink(ctx context.Context, req *fuse.SymlinkRequest) (fs.Node, error) {
429 | logrus.WithField("req", req).Debugln("handling Dor.Symlink call")
430 | return d.Dir, nil
431 | }
432 |
433 | var _ fs.NodeLinker = (*Dir)(nil)
434 |
435 | // Link Dir
436 | func (d *Dir) Link(ctx context.Context, req *fuse.LinkRequest, old fs.Node) (fs.Node, error) {
437 | logrus.WithField("req", req).Debugln("handling Dir.Link call")
438 | return d.Dir, nil
439 | }
440 |
--------------------------------------------------------------------------------
/fs/file.go:
--------------------------------------------------------------------------------
1 | package fs
2 |
3 | import (
4 | "bazil.org/fuse"
5 | "bazil.org/fuse/fs"
6 | "context"
7 | "github.com/pkg/sftp"
8 | "github.com/sirupsen/logrus"
9 | "log"
10 | "sync"
11 | "time"
12 | )
13 |
14 | // File Node
15 | type File struct {
16 | *Node
17 | create bool
18 | file *sftp.File
19 | writing bool
20 | sync.Mutex
21 | }
22 |
23 | var _ fs.Node = (*File)(nil)
24 |
25 | // Attr File
26 | func (f *File) Attr(ctx context.Context, a *fuse.Attr) error {
27 | logrus.Debug("handling File.Attr call")
28 | stat, err := f.sftp.Stat(f.Path())
29 | if err != nil {
30 | return err
31 | }
32 |
33 | statT, ok := stat.Sys().(*sftp.FileStat)
34 | if ok {
35 | a.Atime = time.Unix(int64(statT.Atime), 0)
36 | }
37 |
38 | a.Inode = f.GetInode()
39 | a.Mode = stat.Mode()
40 | a.Size = uint64(stat.Size())
41 | a.Ctime = stat.ModTime()
42 | a.Mtime = stat.ModTime()
43 | return nil
44 | }
45 |
46 | var _ fs.NodeSetattrer = (*File)(nil)
47 |
48 | // Setattr File
49 | func (f *File) Setattr(ctx context.Context, req *fuse.SetattrRequest, resp *fuse.SetattrResponse) error {
50 | logrus.WithField("req", req).Debug("handling File.Setattr call")
51 | if req.Valid.Size() {
52 | resp.Attr.Size = req.Size
53 | return f.sftp.Truncate(f.Path(), int64(req.Size))
54 | }
55 | return nil
56 | }
57 |
58 | var _ fs.NodeOpener = (*File)(nil)
59 |
60 | // Open File
61 | func (f *File) Open(ctx context.Context, req *fuse.OpenRequest, resp *fuse.OpenResponse) (fs.Handle, error) {
62 | logrus.WithField("req", req).Debug("handling File.Open call")
63 | // Unsupported flags
64 | if req.Flags&fuse.OpenAppend == fuse.OpenAppend {
65 | return nil, fuse.ENOTSUP
66 | }
67 |
68 | file, err := f.sftp.OpenFile(f.Path(), int(req.Flags))
69 | if err != nil {
70 | return nil, err
71 | }
72 |
73 | fh := &File{
74 | file: file,
75 | Node: f.Node,
76 | }
77 | fh.Lock()
78 |
79 | if req.Flags.IsReadOnly() {
80 | return fh, err
81 | }
82 |
83 | if req.Flags.IsWriteOnly() {
84 | resp.Flags = fuse.OpenPurgeAttr
85 | return fh, nil
86 | }
87 |
88 | return nil, fuse.ENOTSUP
89 | }
90 |
91 | var _ fs.Handle = (*File)(nil)
92 |
93 | var _ fs.HandleReader = (*File)(nil)
94 |
95 | // Read File
96 | func (f *File) Read(ctx context.Context, req *fuse.ReadRequest, resp *fuse.ReadResponse) error {
97 | logrus.WithField("req", req).Debug("handling File.Read call")
98 | // TODO: 大文件按照 req.Offset 分块读取
99 | if f.file == nil {
100 | var err error
101 | f.file, err = f.sftp.OpenFile(f.Path(), int(req.Flags))
102 | if err != nil {
103 | return err
104 | }
105 | }
106 |
107 | f.file.Seek(req.Offset, 0)
108 | resp.Data = make([]byte, req.Size)
109 | f.file.Read(resp.Data)
110 | return nil
111 | }
112 |
113 | var _ fs.HandleWriter = (*File)(nil)
114 |
115 | // Write File
116 | func (f *File) Write(ctx context.Context, req *fuse.WriteRequest, resp *fuse.WriteResponse) error {
117 | logrus.Debug("handling File.Write call")
118 | var err error
119 | if f.file == nil {
120 | f.file, err = f.sftp.OpenFile(f.Path(), int(req.FileFlags)|int(req.Flags))
121 | if err != nil {
122 | return err
123 | }
124 | }
125 | //stat, err := f.file.Stat()
126 | _, err = f.file.Write(req.Data)
127 | resp.Size = len(req.Data)
128 | return err
129 | }
130 |
131 | var _ fs.NodeFsyncer = (*File)(nil)
132 |
133 | // Fsync File
134 | func (f *File) Fsync(ctx context.Context, req *fuse.FsyncRequest) error {
135 | logrus.Debug("handling File.Fsync call")
136 | return nil
137 | }
138 |
139 | var _ fs.HandleReleaser = (*File)(nil)
140 |
141 | // Release File
142 | func (f *File) Release(ctx context.Context, req *fuse.ReleaseRequest) error {
143 | logrus.Debug("handling File.Release call", f.Path())
144 | var err error
145 | if f.file != nil {
146 | err = f.file.Close()
147 | }
148 | f.Unlock()
149 | return err
150 | }
151 |
152 | var _ fs.HandleFlusher = (*File)(nil)
153 |
154 | // Flush File
155 | func (f *File) Flush(ctx context.Context, req *fuse.FlushRequest) error {
156 | log.Println("Flushing file", f.Path())
157 | return nil
158 | }
159 |
--------------------------------------------------------------------------------
/fs/fs.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2016 Asteris, LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package fs
16 |
17 | import (
18 | "bazil.org/fuse"
19 | "bazil.org/fuse/fs"
20 | "context"
21 | "errors"
22 | "github.com/pkg/sftp"
23 | "github.com/sirupsen/logrus"
24 | "golang.org/x/crypto/ssh"
25 | "log"
26 | "os"
27 | "syscall"
28 | "time"
29 | )
30 |
31 | // SSHFS is a ssh filesystem
32 | type SSHFS struct {
33 | *sftp.Client
34 | root string
35 | conn *fuse.Conn
36 | mountpoint string
37 | }
38 |
39 | // NewSftp sftp
40 | func NewSftp(config *ssh.ClientConfig, server string) (*sftp.Client, error) {
41 | conn, err := ssh.Dial("tcp", server, config)
42 | if err != nil {
43 | panic("Failed to dial: " + err.Error())
44 | }
45 | return sftp.NewClient(conn)
46 | }
47 |
48 | var _ fs.FS = (*SSHFS)(nil)
49 |
50 | // New returns a new SSHFS
51 | func New(config *ssh.ClientConfig, mountpoint, server, root string) (*SSHFS, error) {
52 | client, err := NewSftp(config, server)
53 | if err != nil {
54 | return nil, err
55 | }
56 | sshfs := &SSHFS{
57 | Client: client,
58 | root: root,
59 | mountpoint: mountpoint,
60 | }
61 | return sshfs, nil
62 | }
63 |
64 | // Mount the FS at the given mountpoint
65 | func (v *SSHFS) Mount() error {
66 | var err error
67 |
68 | // 初始化 INode
69 | fileinfo, err := os.Stat(v.mountpoint)
70 | if err != nil {
71 | log.Fatalln(err)
72 | }
73 |
74 | stat, ok := fileinfo.Sys().(*syscall.Stat_t)
75 | if !ok {
76 | log.Fatalf("%+v", fileinfo.Sys())
77 | }
78 | InitInode(stat.Ino)
79 |
80 | v.conn, err = fuse.Mount(
81 | v.mountpoint,
82 | fuse.FSName("ssh"),
83 | fuse.VolumeName("ssh"),
84 | fuse.AsyncRead(),
85 | fuse.WritebackCache(),
86 | fuse.DefaultPermissions(),
87 | fuse.AllowDev(),
88 | fuse.AllowOther(),
89 | //fuse.AllowRoot(),
90 | )
91 |
92 | logrus.Debug("created conn")
93 | if err != nil {
94 | return err
95 | }
96 |
97 | logrus.Debug("starting to serve")
98 | return fs.Serve(v.conn, v)
99 | }
100 |
101 | // Unmount the FS
102 | func (v *SSHFS) Unmount() error {
103 | if v.conn == nil {
104 | return errors.New("not mounted")
105 | }
106 |
107 | err := fuse.Unmount(v.mountpoint)
108 | if err != nil {
109 | return err
110 | }
111 |
112 | err = v.conn.Close()
113 | if err != nil {
114 | return err
115 | }
116 |
117 | logrus.Debug("closed connection, waiting for ready")
118 | <-v.conn.Ready
119 | if v.conn.MountError != nil {
120 | return v.conn.MountError
121 | }
122 |
123 | return nil
124 | }
125 |
126 | // Root returns the struct that does the actual work
127 | func (v *SSHFS) Root() (fs.Node, error) {
128 | logrus.Debug("returning root")
129 | root := NewRoot(v.root, v.Client)
130 | root.localpath = v.mountpoint
131 | return root.Dir, nil
132 | }
133 |
134 | var _ fs.FSStatfser = (*SSHFS)(nil)
135 |
136 | // Statfs sshfs
137 | func (v *SSHFS) Statfs(ctx context.Context, req *fuse.StatfsRequest, resp *fuse.StatfsResponse) error {
138 | logrus.Debug("handling SSHFS.Statfs call")
139 | return nil
140 | }
141 |
142 | // timespecToTime
143 | func timespecToTime(ts syscall.Timespec) time.Time {
144 | return time.Unix(int64(ts.Sec), int64(ts.Nsec))
145 | }
146 |
--------------------------------------------------------------------------------
/fs/node.go:
--------------------------------------------------------------------------------
1 | package fs
2 |
3 | import (
4 | "encoding/json"
5 | kv "github.com/patrickmn/go-cache"
6 | "github.com/pkg/sftp"
7 | "github.com/sirupsen/logrus"
8 | sq "github.com/yireyun/go-queue"
9 | "io"
10 | "net/http"
11 | "path/filepath"
12 | "strconv"
13 | )
14 |
15 | var ginode uint64 = 900000000
16 | var freeInode = sq.NewQueue(1000000)
17 | var inodeCache = kv.New(kv.DefaultExpiration, kv.NoExpiration)
18 |
19 | // DebugServer hello world, the web server
20 | func DebugServer(w http.ResponseWriter, req *http.Request) {
21 | jb, err := json.Marshal(&struct {
22 | FreeInode string `json:"free_inode"`
23 | Count int `json:"count"`
24 | Items map[string]kv.Item `json:"items"`
25 | }{
26 | FreeInode: freeInode.String(),
27 | Count: inodeCache.ItemCount(),
28 | Items: inodeCache.Items(),
29 | })
30 | if err != nil {
31 | io.WriteString(w, err.Error())
32 | return
33 | }
34 | io.WriteString(w, string(jb))
35 | }
36 |
37 | //func init() {
38 | // http.HandleFunc("/debug", DebugServer)
39 | // go func() {
40 | // err := http.ListenAndServe(":12345", nil)
41 | // if err != nil {
42 | // log.Fatal("ListenAndServe: ", err)
43 | // }
44 | // }()
45 | //}
46 |
47 | // InitInode inode
48 | func InitInode(inode uint64) {
49 | ginode = inode
50 | }
51 |
52 | // genInode inode
53 | func genInode() uint64 {
54 | //val, ok, _ := freeInode.Get()
55 | //if ok {
56 | // return val.(uint64)
57 | //}
58 | ginode++
59 | return ginode
60 | }
61 |
62 | // Node 文件系统节点,用于描述目录或者文件的文件系统属性
63 | type Node struct {
64 | inode uint64
65 | name string // 名称,如:test
66 | path string // 远程服务器的目录,如:"/tmp/test"
67 | localpath string // 本地绝对路径
68 | isdir bool
69 | isroot bool
70 | parent *Node
71 | *File
72 | *Dir
73 | sftp *sftp.Client
74 | }
75 |
76 | // MarshalJSON 自定义序列化
77 | func (n *Node) MarshalJSON() ([]byte, error) {
78 | type node struct {
79 | Name string `json:"name"`
80 | Inode uint64 `json:"inode"`
81 | ParentInode uint64 `json:"parent_inode"`
82 | ParentName string `json:"parent_name"`
83 | }
84 |
85 | var s = struct {
86 | FileCount int `json:"files_count"`
87 | DirsCount int `json:"dirs_count"`
88 | Inode uint64 `json:"inode"`
89 | Name string `json:"name"`
90 | Parent uint64 `json:"parent"`
91 | Type string `json:"type"`
92 | LocalPath string `json:"local_path"`
93 | RemotePath string `json:"remote_path"`
94 | Files []node `json:"files,omitempty"`
95 | Dirs []node `json:"dirs,omitempty"`
96 | }{
97 | Inode: n.inode,
98 | Name: n.name,
99 | LocalPath: n.LocalPath(),
100 | Parent: func() uint64 {
101 | if n.isroot {
102 | return 0
103 | }
104 | return n.parent.inode
105 | }(),
106 | FileCount: func() int {
107 | if n.Dir != nil && n.Dir.Files != nil {
108 | return len(*n.Dir.Files)
109 | }
110 | return 0
111 | }(),
112 | DirsCount: func() int {
113 | if n.Dir != nil && n.Dir.Dirs != nil {
114 | return len(*n.Dir.Dirs)
115 | }
116 | return 0
117 | }(),
118 | Type: func() string {
119 | if n.isdir {
120 | if n.isroot {
121 | return "dir:root"
122 | }
123 | return "dir"
124 | }
125 | return "file"
126 | }(),
127 | Files: func() []node {
128 | nodes := []node{}
129 | if n.Dir.Files != nil {
130 | for _, f := range *n.Dir.Files {
131 | nodes = append(nodes, node{
132 | Name: f.name,
133 | Inode: f.inode,
134 | ParentInode: f.parent.inode,
135 | ParentName: f.parent.name,
136 | })
137 | }
138 | }
139 | return nodes
140 | }(),
141 | Dirs: func() []node {
142 | nodes := []node{}
143 | if n.Dir.Dirs != nil {
144 | for _, d := range *n.Dir.Dirs {
145 | nodes = append(nodes, node{
146 | Name: d.name,
147 | Inode: d.inode,
148 | ParentInode: d.parent.inode,
149 | ParentName: d.parent.name,
150 | })
151 | }
152 | }
153 | return nodes
154 | }(),
155 | RemotePath: n.Path(),
156 | }
157 | return json.Marshal(&s)
158 | }
159 |
160 | // IsDir 判断是否目录
161 | func (n *Node) IsDir() bool {
162 | return n.isdir
163 | }
164 |
165 | // IsRoot 判断是否根目录
166 | func (n *Node) IsRoot() bool {
167 | return n.isroot
168 | }
169 |
170 | // GetInode 获取节点 inode
171 | func (n *Node) GetInode() uint64 {
172 | return n.inode
173 | }
174 |
175 | // Save 缓存 FsNode
176 | func (n *Node) Save() {
177 | inodeCache.Set(strconv.FormatUint(n.inode, 10), n, kv.NoExpiration)
178 | if n.parent != nil {
179 | key := strconv.FormatUint(n.parent.inode, 10) + "_" + n.name
180 | inodeCache.Set(key, n, kv.NoExpiration)
181 | }
182 | }
183 |
184 | // Path 获取节点绝对路径
185 | // 从当前节点开始,一直向父节点循环
186 | func (n *Node) Path() string {
187 | path := []string{n.name}
188 | pnode := Node{}
189 | if n.isroot {
190 | return n.path
191 | }
192 | pnode = *n.parent
193 | for !pnode.isroot {
194 | path = append(path, pnode.name)
195 | pnode = *pnode.parent
196 | }
197 | path = append(path, pnode.path)
198 | path = reverse(path)
199 |
200 | return filepath.Join(path...)
201 | }
202 |
203 | // LocalPath 获取本地路径
204 | func (n *Node) LocalPath() string {
205 | path := []string{n.name}
206 | pnode := Node{}
207 | if n.isroot {
208 | return n.localpath
209 | }
210 | pnode = *n.parent
211 | for !pnode.isroot {
212 | path = append(path, pnode.name)
213 | pnode = *pnode.parent
214 | }
215 | path = append(path, pnode.localpath)
216 | path = reverse(path)
217 |
218 | return filepath.Join(path...)
219 | }
220 |
221 | // reverse 数组反转
222 | func reverse(s []string) []string {
223 | for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
224 | s[i], s[j] = s[j], s[i]
225 | }
226 | return s
227 | }
228 |
229 | // NewNode 新增节点
230 | func NewNode(sftp *sftp.Client, inode uint64, parent *Node, name string, isdir, isroot bool) *Node {
231 | logrus.WithFields(map[string]interface{}{
232 | "inode": inode,
233 | "name": name,
234 | "isdir": isdir,
235 | "isroot": isroot,
236 | }).Debugln("NewNode...")
237 | //debug.PrintStack()
238 | node := &Node{
239 | inode: genInode(),
240 | File: &File{},
241 | Dir: &Dir{},
242 | sftp: sftp,
243 | name: name,
244 | isdir: isdir,
245 | isroot: isroot,
246 | parent: parent,
247 | }
248 | node.Dir.Node = node
249 | node.File.Node = node
250 | if isdir && isroot {
251 | node.path = name
252 | node.name = filepath.Base(name)
253 | }
254 |
255 | if inode > 0 {
256 | node.inode = inode
257 | }
258 | node.Save()
259 | return node
260 | }
261 |
262 | // GetNodeByID 根据 id 获取 Node 对象
263 | func GetNodeByID(inode uint64) (*Node, bool) {
264 | c, ok := inodeCache.Get(strconv.FormatUint(uint64(inode), 10))
265 | if !ok {
266 | return nil, ok
267 | }
268 | return c.(*Node), ok
269 | }
270 |
271 | // Rename Node
272 | func (n *Node) Rename(onode, ndir *Node, nname string) {
273 | inodeCache.Delete(strconv.FormatUint(n.inode, 10) + "_" + onode.name)
274 | onode.parent = ndir
275 | onode.name = nname
276 | onode.Save()
277 | }
278 |
279 | // GetChild 根据名称获取子 Node
280 | func (n *Node) GetChild(name string) (*Node, bool) {
281 | key := strconv.FormatUint(n.inode, 10) + "_" + name
282 | node, ok := inodeCache.Get(key)
283 | if !ok {
284 | return nil, ok
285 | }
286 | return node.(*Node), true
287 | }
288 |
289 | // Remove 删除 Node
290 | func (n *Node) Remove() {
291 | inodeCache.Delete(strconv.FormatUint(n.inode, 10))
292 | if n.parent != nil {
293 | inodeCache.Delete(strconv.FormatUint(n.parent.inode, 10) + "_" + n.name)
294 | }
295 | n.rmInode()
296 | }
297 |
298 | // rmInode 释放inode
299 | func (n *Node) rmInode() {
300 | _, _ = freeInode.Put(n.inode)
301 | }
302 |
--------------------------------------------------------------------------------
/fs/ssh.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2016 Asteris, LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package fs
16 |
17 | import (
18 | "golang.org/x/crypto/ssh"
19 | "io/ioutil"
20 | )
21 |
22 | // PublicKeyFile ssh
23 | func PublicKeyFile(file string) (ssh.AuthMethod, error) {
24 | buffer, err := ioutil.ReadFile(file)
25 | if err != nil {
26 | return nil, err
27 | }
28 |
29 | key, err := ssh.ParsePrivateKey(buffer)
30 | if err != nil {
31 | return nil, err
32 | }
33 | return ssh.PublicKeys(key), nil
34 | }
35 |
--------------------------------------------------------------------------------
/fs/utils.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2016 Asteris, LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package fs
16 |
17 | import (
18 | "golang.org/x/crypto/ssh"
19 | "log"
20 | "net"
21 | )
22 |
23 | // NewConfig creates a new config
24 | func NewConfig(user, password string, privateKeyPath string) *ssh.ClientConfig {
25 | auth := []ssh.AuthMethod{
26 | ssh.Password(password),
27 | }
28 |
29 | publicKey, err := PublicKeyFile(privateKeyPath)
30 | if err == nil {
31 | auth = append(auth, publicKey)
32 | } else {
33 | log.Println(err)
34 | }
35 |
36 | return &ssh.ClientConfig{
37 | User: user,
38 | Auth: auth,
39 | Config: ssh.Config{
40 | Ciphers: []string{"aes128-ctr", "aes192-ctr", "aes256-ctr", "aes128-gcm@openssh.com", "arcfour256", "arcfour128", "aes128-cbc", "3des-cbc", "aes192-cbc", "aes256-cbc"},
41 | },
42 | HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
43 | return nil
44 | },
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2016 Asteris, LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import "github.com/soopsio/sshfs-go/cmd"
18 |
19 | func main() {
20 | cmd.Execute()
21 | }
22 |
--------------------------------------------------------------------------------
/release/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM gliderlabs/alpine:3.3
2 |
3 | # build-base
4 | RUN apk add --no-cache build-base
5 |
6 | # go
7 | RUN apk add --no-cache go
8 | RUN mkdir /go
9 | ENV GOPATH /go
10 | ENV GO15VENDOREXPERIMENT 1
11 |
12 | # glide
13 | RUN apk add --no-cache --virtual=glide-deps curl ca-certificates && \
14 | mkdir /tmp/glide && \
15 | curl -L https://github.com/Masterminds/glide/releases/download/0.9.3/glide-0.9.3-linux-amd64.tar.gz | tar -xzv -C /tmp/glide && \
16 | apk del glide-deps && \
17 | mv /tmp/glide/**/glide /bin/glide
18 |
--------------------------------------------------------------------------------
/release/build.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | if [ ! -d .git ]; then
4 | echo "this script should be run from the root of the project"
5 | exit 1
6 | fi
7 |
8 | if ! docker images | grep -q sshfs-build; then
9 | docker build -t sshfs-build release/
10 | fi
11 |
12 | docker run --rm -v $(pwd):/go/src/github.com/soopsio/sshfs-go sshfs-build /bin/sh -c 'cd /go/src/github.com/soopsio/sshfs-go && env GOOS=linux GOARCH=amd64 go build .'
13 |
--------------------------------------------------------------------------------
/scripts/gofmtcheck.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Check gofmt
4 | echo "==> Checking that code complies with gofmt requirements..."
5 | gofmt_files=$(gofmt -l . | grep -v vendor)
6 | if [[ -n ${gofmt_files} ]]; then
7 | echo 'gofmt needs running on the following files:'
8 | echo "${gofmt_files}"
9 | echo "You can use the command: \`make fmt\` to reformat code."
10 | exit 1
11 | fi
12 |
13 | exit 0
14 |
--------------------------------------------------------------------------------
/scripts/golint.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | EXIT=0
4 |
5 | # Check gofmt
6 | echo "==> Checking that code complies with golint requirements..."
7 | packages=$@
8 |
9 | for package in $@; do
10 | OUT=$(golint $package)
11 |
12 | if [ "$OUT" != "" ]; then
13 | echo -e "$OUT"
14 | EXIT=1
15 | fi
16 | done
17 |
18 | exit $EXIT
19 |
--------------------------------------------------------------------------------