├── .gitignore ├── LICENSE ├── README.md ├── cli.go ├── config.go ├── config.toml ├── docs ├── api.md ├── config.md ├── diagram │ ├── client.drawio │ ├── client.jpg │ ├── demux.drawio │ ├── demux.jpg │ └── sender&receiver.drawio ├── reports │ └── v0.1.0实现.pdf ├── rsync_subprotocol.md └── v0.2.x_Zh-CN.md ├── go.mod ├── go.sum ├── main.go ├── rsync ├── attribs.go ├── chksum.go ├── client.go ├── compress.go ├── connection.go ├── const.go ├── demux.go ├── demux_old.go ├── exclude.go ├── flist.go ├── fs.go ├── receiver.go ├── sender.go ├── ssh.go └── utils.go └── storage ├── cache ├── finfo.pb.go ├── finfo.proto ├── pbgen.sh ├── redis.go └── utils.go ├── local.go └── minio.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | .idea/ 18 | 19 | # boltdb 20 | *.db -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RSYNC-OS 2 | ## A port of rsync written in Go, with object storage support 3 | 4 | # Introduction 5 | rsync-os is a Golang implementation of Rsync built from scratch. Its goal is to be a modern rsync-compatible tool with features like: 6 | * Supports file storage and object storage 7 | * Uses a subset of rsync wire protocol to avoid blocks efficient transfer (For sender and receiver) 8 | * Rsync APIs 9 | * Safe 10 | 11 | ![client](https://raw.githubusercontent.com/kaiakz/rsync-os/master/docs/client.jpg) 12 | 13 | ## Usage 14 | ### Use minio as backend 15 | 1. install & run minio, you need to configure the `config.toml`. 16 | 2. `go build` 17 | 3. `./rsync-os rsync://[USER@]HOST[:PORT]/SRC minio`, for example, `./rsync-os rsync://mirrors.tuna.tsinghua.edu.cn/ubuntu minio` 18 | 19 | ## Roadmap 20 | ### Client 21 | #### Rsync wire protocol 27: 22 | - [x] Connect to rsync daemon by rsync://: Handshake, Fetches the file list, Requests & download files 23 | - [ ] Handles error 24 | #### Storage backend 25 | - [x] Minio: supports regular files, supports folder & symlink 26 | - [x] Minio: caches file list in boltdb 27 | - [ ] Local 28 | #### Other 29 | - [x] CLI 30 | 31 | ## Detailed Information 32 | #### openrsync has a really good [documentation](https://github.com/kristapsdz/openrsync/blob/master/README.md) to describe how rsync algorithm works. 33 | 34 | #### Why do this? 35 | Just as [rsyn](https://github.com/sourcefrog/rsyn#why-do-this) said, "The rsync C code is quite convoluted, with many interacting options and parameters stored in global variables affecting many different parts of the control flow, including how structures are encoded and decoded." I would like to provide a rsync written in clean and understandable Golang code. 36 | 37 | rsync has [a bad performance](https://github.com/tuna/rsync/blob/master/README-huai.md). Inspired by rsync-huai, rsync-os stores the file list in database to avoid recursively generating the list. 38 | 39 | Modernized rsync: rsync-os supports both file storage and object storage. 40 | 41 | #### What's the difference between rsync and rsync-os 42 | rsync-os is the express edition of rsync, with object storage support. It uses a subset of rsync wire protocol(without rolling block checksum). 43 | 44 | #### rsync-os and rclone are completely different 45 | rclone does not support rsync wire protocol although it is called "rsync for object storage". With rclone you can't transfer files between rsync and object storage. 46 | 47 | #### Why we don't need rolling block checksum for regular file? 48 | In the rsync algorithm, rsync requires random access of files to do the block exchange. But object storage does not support that. 49 | 50 | rsync-os simplifies the rsync algorithm to avoid random reading and writing, since rsync-os don't need to do a rolling checksum scanning the file. 51 | 52 | As a client, when a file has different size or modified time compared to the remote file, rsync-os just pretend 'the file does not exist here', then send a reply to download the entire file from the server and finally replace it. 53 | 54 | As a server, TBC 55 | 56 | #### HandShake 57 | rysnc-os supports rsync protocol 27. 58 | Now it sends the arguments "--server--sender-l-p-r-t" to the remote rsyncd by default. 59 | 60 | #### The File List 61 | According to the arguments rsync-os sent, the file list should contain path, size, modified time & mode. 62 | 63 | #### Request the file 64 | rsync-os always saves the file list in its database(local file list). rsync2os doesn't compare each file with the file list from remote server(remote file list), but the latest local file list. If the file in the local file list has different size, modified time, or doesn't exist, rsync2os will download the whole file(without [block exchange](https://github.com/kristapsdz/openrsync#block-exchange)). To to do that, rsync2os sends the empty block checksum with the file's index of the remote file list. 65 | 66 | #### Download the file 67 | The rsync server sends the entire file as a stream of bytes. 68 | 69 | #### Multiplex & De-Multiplex 70 | Most rsync transmissions are wrapped in a multiplexing envelope protocol. The code written in C to multiplex & de-multiplex is obscure. 71 | Unlike rsync, rsync-os reimplements this part: It just does multiplexing & de-multiplexing in a goroutine. 72 | ![de-multiplex](https://raw.githubusercontent.com/kaiakz/rsync-os/master/docs/demux.jpg) 73 | 74 | ### Limitations 75 | * Do not support block exchange for regular files. If a file was modified, just downloads the whole file. 76 | * rsync-os can only act as client/receiver now. 77 | 78 | # Reference 79 | * [rsync](https://rsync.samba.org/) 80 | * [openrsync](https://github.com/openbsd/src/tree/master/usr.bin/rsync), a BSD-liscesed rsync 81 | * [rsync-huai](https://github.com/tuna/rsync), a modified version rsync by Tsinghua University TUNA Association 82 | * [yajsync](https://github.com/perlundq/yajsync), a port of rsync written in Java 83 | * [rsyn](https://github.com/sourcefrog/rsyn), wire-compaible rsync in Rust 84 | * [acrosync-library](https://github.com/gilbertchen/acrosync-library) 85 | * [repositoryd](https://github.com/APNIC-net/repositoryd), An rsync-compatible RPKI repository daemon. 86 | * https://rsync.samba.org/resources.html 87 | * https://github.com/boundary/wireshark/blob/master/epan/dissectors/packet-rsync.c 88 | * https://tools.ietf.org/html/rfc5781 -------------------------------------------------------------------------------- /cli.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type Cli struct { 4 | Src string 5 | Dest string 6 | // Flags -aDglnoprtvx 7 | } 8 | 9 | /* 10 | Usage: rsync [OPTION]... SRC [SRC]... DEST 11 | or rsync [OPTION]... SRC [SRC]... [USER@]HOST:DEST 12 | or rsync [OPTION]... SRC [SRC]... [USER@]HOST::DEST 13 | or rsync [OPTION]... SRC [SRC]... rsync://[USER@]HOST[:PORT]/DEST 14 | or rsync [OPTION]... [USER@]HOST:SRC [DEST] 15 | or rsync [OPTION]... [USER@]HOST::SRC [DEST] 16 | or rsync [OPTION]... rsync://[USER@]HOST[:PORT]/SRC [DEST] 17 | */ 18 | //func ParseArgs() *Cli { 19 | //cli := &Cli{} 20 | 21 | //syncCommand := flag.NewFlagSet("sync", flag.ExitOnError) 22 | //recoverCommand := flag.NewFlagSet("recover", flag.ExitOnError) 23 | //lsCommand := flag.NewFlagSet("ls", flag.ExitOnError) 24 | //testCommand := flag.NewFlagSet("test", flag.ExitOnError) 25 | 26 | //} -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/spf13/viper" 5 | "io/ioutil" 6 | "log" 7 | ) 8 | 9 | // Create a default config file if not found config.toml 10 | func loadConfigIfExists() { 11 | viper.SetConfigName("config") // name of config file (without extension) 12 | viper.SetConfigType("toml") // REQUIRED if the config file does not have the extension in the name 13 | //viper.AddConfigPath("/etc/rsync-os/") // path to look for the config file in 14 | //viper.AddConfigPath("$HOME/.rsync-os") // call multiple times to add many search paths 15 | viper.AddConfigPath(".") // optionally look for config in the working directory 16 | 17 | err := viper.ReadInConfig() // Find and read the config file 18 | if err != nil { // Handle errors reading the config file 19 | if _, ok := err.(viper.ConfigFileNotFoundError); ok { 20 | // Config File not found 21 | 22 | createSampleConfig() 23 | log.Fatalln("Config does not exist, a sample of config was created") 24 | } else { 25 | // Found but got errors 26 | log.Fatalln(err) 27 | } 28 | } 29 | } 30 | 31 | func createSampleConfig() { 32 | confSample := []byte( 33 | `title = "configuration of rsync-os" 34 | 35 | # [object storage's name] 36 | [minio] 37 | endpoint = "127.0.0.1:9000" 38 | keyAccess = "minioadmin" 39 | keySecret = "minioadmin" 40 | [minio.boltdb] 41 | path = "test.db" 42 | `) 43 | 44 | if ioutil.WriteFile("config.toml", confSample, 0666) != nil { 45 | log.Fatalln("Can't create a sample of config") 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /config.toml: -------------------------------------------------------------------------------- 1 | title = "configuration of rsync-os" 2 | 3 | # [object storage's name] 4 | [minio] 5 | endpoint = "127.0.0.1:9000" 6 | keyAccess = "minioadmin" 7 | keySecret = "minioadmin" 8 | [minio.boltdb] 9 | path = "test.db" 10 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | # rsync API 2 | **STATUS: UNSTABLE** With the API, you can create an rsync receiver/sender and customize how it works by your arguments and callbacks. 3 | 4 | ### Preknowledge 5 | https://rsync.samba.org/how-rsync-works.html 6 | 7 | ## v0.2.0 8 | ### Client 9 | #### receiver: 10 | First of all, you need to implement the rsync.FS interface to specify a storage backend used for the task. 11 | ```go 12 | type FS interface { 13 | Put(fileName string, content io.Reader, fileSize int64, metadata FileMetadata) (written int64, err error) 14 | Get(fileName string, metadata FileMetadata) (File, error) 15 | Delete(fileName string, mode FileMode) error 16 | List() (FileList, error) 17 | Stats() (seekable bool) 18 | } 19 | ``` 20 | Then, decide what arguments you would like to use: 21 | ```go 22 | type Attribs struct { 23 | sender bool // --sender 24 | server bool // --server 25 | recursive bool // -r 26 | dryRun bool // -n 27 | hasTimes bool // -t 28 | hasPerms bool // -p 29 | hasLinks bool // -l 30 | hasGID bool // -g 31 | hasUID bool // -u 32 | } 33 | ``` 34 | 35 | ```go 36 | // via socket 37 | func SocketClient(storage FS, address string, module string, path string, options map[string]string) (SendReceiver, error) 38 | 39 | // via ssh 40 | func SshClient(storage FS, address string, module string, path string, options map[string]string) (SendReceiver, error) 41 | ``` 42 | Call `Run()` for the receiver to start a syncing task. 43 | -------------------------------------------------------------------------------- /docs/config.md: -------------------------------------------------------------------------------- 1 | # Configration Format 2 | **NOTE** The format is unstable, it may be changed in the future version. 3 | The `config.toml` is the runtime configuration for rsync-os (both daemon/server and client). Similiar to `rsyncd.conf`, it can control authentication, access, logging, available modules and which storage backend rsync-os uses(local file storage, s3 object storage). 4 | 5 | A storage backend begins with the name of the baackend in the square brackets and continues until the next square brackets. 6 | 7 | * To configure a s3 object storage: 8 | ```toml 9 | [minio] 10 | endpoint = "192.168.11.192:9000" # Require 11 | keyAccess = "minioadmin" # Require 12 | keySecret = "minioadmin" # Require 13 | mods = [] # Unlike rsync, all module will specify in the storage backend. 14 | ```` 15 | 16 | -------------------------------------------------------------------------------- /docs/diagram/client.drawio: -------------------------------------------------------------------------------- 1 | 5Vhbb9owFP41SNsDU0hCWh5XoO2u0kQ7HicTm8TFsTPH3PbrZ8fOxcRj6za2jkkI4nNOHM73nZvTC8bZ7oaDPH3HICI934O7XjDp+f4g9P2e+nhwryUXwUALEo6hMWoEM/wFGaFnpGsMUWEZCsaIwLktjBmlKBaWDHDOtrbZkhH7qTlIUEcwiwHpSucYilRLL/2LRn6LcJJWTx5EI63JQGVsPClSANm2JQqmvWDMGRP6KtuNEVHgVbjMX+3n5O0qunn9ofgM7q/e3L3/2NebXT/mltoFjqj46a1j/Gn+aTSa7Fej+zf8Yf56Cry+IXcDyNrgZXwV+wpABCWeZsm4SFnCKCDTRnrF2ZpCpB7jyVVj85axXAoHUviAhNib4ABrwaQoFRkx2h/0z+BQsDWP0RG7wIQZ4AkSR5w3vCoHW7Fi0LtBLEOC76UBRwQIvLEDCpi4TGq7Bnt5YeB/BMtBh4pbQKEMuhXqcNIgruDbpligWQ5KULYyjW10l4wKA/1AuneVEFAUhqxCcLaqE0NZ11HuPZqYDeIC7Y5CWWkjg/3eLhbbJkMHVdqlrewMvV8H3xkK1R84r0SIuongtLv4W4ng5iL8T7hweh88qaIUOaiIiFB1IgfU4iT6vFadsCw3veCl1A38fNdTaXytFi/88joCWV6iGwShAhyRDRI4Bh2Nsi6/ZU0RfUBwQvW2sSQE8VodM8K41vBk8cwrnyK99VpXz+3NBAe0WDKe6dsoo8g2wDJ0Ki887USpJDJiEO9L32NMk+pungFi3w9RzLikh1HHE7aMQ3uL9iMgLnIC9lqBKcGtO5eEAeHYkcnKK3Xbvi7+jn+1APEqKVOif4CXH17WOPnhqLkeKtQqUuVVon7HLFMZofxM1beMQVQI9dcwKde4XGEVGjGIlY0OlwWvtqgkMix0DFXiwx6XsmyxLr7f36yOpaLvGmSYqM5yWwdXtwuagJqYaHK2woqjSdSs7sry0Q9P2R2HQ7s7Vut2ewwc7XH0G9rjsQbRKgOzFc7PbiwJLg6Av/yDc4nbhe6Afp9DmXR1Bn478c6MnEFoz4yBi5zIQc7wZENjd2T/twaVY6ei7w6N0ZMaGrvz+zUScdpJkCVnmUIB8Y2aIc4tRw7OVT9av06XIq4B8p9PEccLhqPV+4nkSLdcTdiWypkSWt2k0APgOm80KmUkpnLSXDyol3SKPzngJufYZYYv7CHAmUThqJtE0cmSaOhIInNaootC/Ux3urzV8uos5c1ApogtFMwlr5nAWZc3dWw5GKtLAsb6tDApzxuSMUzIgagzSCuw5cxNXhpFhiEs09gVDXZqQ1CkdfCcimL/8mDKi7oEjxxFMng8v3LZvB8uda237MH0Kw== -------------------------------------------------------------------------------- /docs/diagram/client.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaiakz/rsync-os/64e84daeabb1fa4d2c7cf766c196306adfba6cb2/docs/diagram/client.jpg -------------------------------------------------------------------------------- /docs/diagram/demux.drawio: -------------------------------------------------------------------------------- 1 | 5ZjbctowEIafxpfM+IAhXNaQkqahMw0lXCu2YqvIWiqvOeTpK2EZY+whh07SlM5wgX4tMvv92hXC8obpZizJMplARLnl2tHG8kaW6zpd17X0y462hdLv9QohliwyQZUwZY/UiLZRcxbRrBaIABzZsi6GIAQNsaYRKWFdD3sAXn/qksS0IUxDwpvqnEWYFOqF26/0K8ripHyy0xsUMykpg00mWUIiWB9I3qXlDSUAFu/SzZByDa/kMv+ynfObRW98/T37RWbB1x/f7jrFYp9f8pF9CpIKfPXSPgbJoiNugtkMHu9Gs9vw6rrjFUuvCM8NL5MrbkuANFI8zRAkJhCDIPyyUgMJuYiofoytRlXMDcBSiY4Sf1LErdkcJEdQUoIpN7PPzM9wyCCXIT0RZ5JCImOKJ5I3ueoED/aKoTemkFKUWxUgKSfIVvUNRcy+jPdxFXv1xuB/gctNK26zrQh1wlThlQ1jKuya4TphSKdLsiOzVrVcR/wAAg1/R+UYxJxkmXEsQwmLfXXo6P1Wt1/szopKpJuTPM1s19SVaSz7OltXZeqUWnJQohf2nzvQuh+cdgc6kJ0de9/5aPC7/3gnOtVhnuxEzt/qRKe+9YEVIzrJN8OEiLMrBLfnf7BC8Bv0x6AoIxO0QV8liXXEBcYhcJBKESB0WTwwzo8kwlks1DBUPNXZ4gUaGVM/mz6ZiZRF0a6m2jyt19mbeTOoe+M5TW96Lda4b2VNv2HNFMKFquynfXk+b0kz9kjud0tpuktgAneZ+IHlj/Raqm1lpozeEL8zODoj+k38/nviv2jg1y1J6AvLOfLv2oMn+b/r9h80+E9yfZHjdKN6gboiEiTnaYVzfEp4LaeE/Z5elK2wdkin/4kd3nFnarOj7dB+hR1qWF3xd3MHf5R4l78B -------------------------------------------------------------------------------- /docs/diagram/demux.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaiakz/rsync-os/64e84daeabb1fa4d2c7cf766c196306adfba6cb2/docs/diagram/demux.jpg -------------------------------------------------------------------------------- /docs/diagram/sender&receiver.drawio: -------------------------------------------------------------------------------- 1 | 7VhdU6MwFP01fVyHj9LSR9tad2fsrqPO6D7tRLiWTAPBEJT66zeBAKXBtlbR/bAPbXISLuGcc3NDe/YkzE4ZioM59YH0LMPPeva0Z1kj1xTfElgVgGO5BbBg2C8gswYu8RMo0FBoin1IGhM5pYTjuAl6NIrA4w0MMUYfm9PuKGneNUYL0IBLDxEdvcY+DwrUdYwa/wp4EZR3Ng01EqJysgKSAPn0cQ2yT3r2hFHKi1aYTYBI7kpeiutmz4xWC2MQ8X0usKN0PMvYyFudGXH04/rKQqdfVJQHRFL1wBfgYXgAphbNVyUTYv2xbKYhOcN3QHAkeuMYGA6Bi/n2lCj4vMbGQheOBCbHzbxPCIoTfJuHNQTCwEtZgh/gApJC/hylaeSDr3oVd3mHM7qs1JBBdSrK5wLGIVuDFDWnQMUC2UpMKUdLmZRPXdV9rEWvpA3WBLdLECmjLarQtRaioeR4gTSWJo0mSUxxxPP7OuOeM92QgzIe0AWNEFkX5JXEbnXR3mz3nQbZwzayda4r8M25djSuL0Gw9L8lQWXmPyYJBn9PEjgvZXsjCVrJbkmCzrgealzPMBG1Ny+uXD68ZVAmyQdYajLI58aibh4TvIgEdEs5p6GgTyTSsSzEUowYxNDYR0mQk28Ww6rqu7KXYX6jlJDtn7J9NHJeYf+EpsyD3R7jiC1gm8DKeuA3Tgy6vAwI4iKVm2eRFrXUpefSwLUtrGHTF5Y9aIYoFqqu2tC8WsbhNhi1HAnuU0jkEpUhDpD+llBv2aWK1p4qDt5HRdNtbqVa2hYPpKmo26G/I1DHdnBbdwWBnOGEd2mF1q1AtafZemelOv/sFmAOOtN8+Avfo/7sLl3ZprlMh1dPLGt5K/gmymUmL4vULvAaA+xfBswDy8CGQC2meHPN7L5zZKx9zGbWDkZHo/XPXnru3lW2h31mj3krm/Q1m4zz1LaMSQDeMknDT3vsaQ+3G3tsD9uxPfTKIV+q3u8U8TEqNwSw7QOr/qaSWqCuK4ChifcdhWLXn8w5Ln6pL386zvBD3wM+QPtNycxhM/kG+x3jdU85L4vbcVab+uFgPhWVYDafOp9meNYMo47MsCPuwWYQ3fqv6WJ6/f++ffIb -------------------------------------------------------------------------------- /docs/reports/v0.1.0实现.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaiakz/rsync-os/64e84daeabb1fa4d2c7cf766c196306adfba6cb2/docs/reports/v0.1.0实现.pdf -------------------------------------------------------------------------------- /docs/rsync_subprotocol.md: -------------------------------------------------------------------------------- 1 | # Rsync subprotocol 2 | A subset of rsync protocol without block checksum, designed for the compatibility of any version of standard rsync receiver/sender (client/server). 3 | 4 | ## Introdution 5 | In the rsync algorithm, rsync requires random access of files to do the block exchange. It will cause a high CPU and disk usage. `rsync subprotocol` simplifies the block exchange to avoid that. It can **work with sequential files**. 6 | 7 | ### **NOTICE** Although `rsync subprotocol` don't need to do rolling checksum, a whole-file MD4/MD5 checksum is still required to validate the transfer. We can compute MD5 checksums during receiving/sending files. 8 | 9 | ## Receiver Side 10 | In the `rsync algorithm`, after the receiver reads the file list, it interates through each file in the list and handles three situations when processes each file: 11 | 1. same as remote file: skip 12 | 2. exists but was modified (has different size or last modification time): the receiver examines each file in blocks of a fixed size. Each block will be hashed twice (Adler-32 and MD4/MD5), and sent to the sender. 13 | 3. does not exist or empty: can't hash blocks of file, so just send a zero block to require the entire file. 14 | 15 | But in `rsync subprotocol`, receiver do not support block checksum, so it won't send any hashes to the sender, but zero blocks instead. For a modified file, receiver always pretends `the file does not exist` to get the entire file from sender. **Just like you specify the argument `--whole-file` in standard rsync.** 16 | 17 | 18 | ## Sender Side 19 | In the `rsync algorithm`, when receiver find a modfied file, it will send the file's block hashes to sender. Once accepted, the sender will also calculate the hashes of each file's blocks: 20 | 1. If a block is matched, just send the index of the block to receiver so receiver will copy relative data from the file locally. 21 | 2. If a block is not matched, it means the block from receiver was modified. The block of the file will be sent as a stream of bytes, and receiver will copy it to file directly. 22 | 23 | In `rsync subprotocol`, sender will ignore any block that comes from receiver so there are no matched blocks. For each file required by receiver, sender always sends the entire file. -------------------------------------------------------------------------------- /docs/v0.2.x_Zh-CN.md: -------------------------------------------------------------------------------- 1 | # v0.2.x 2 | 在开始之前首先明确,主动建立连接的一方是client,被动连接的一方是server。sender负责发送文件,receiver负责接收文件,rsync算法之中sender和receiver是配对的,而client和server会分别成为二者之一。 3 | 4 | # 实现协议v27,参考的rsync版本v2.6.0 5 | 6 | ## 目标 7 | v0.2.x的目标是实现一套高可定制的rsync API,用户基于这套API可以自由创建以及定制rsync的sender以及receiver。在API约定下,支持包括连接方式(Socket,SSH等),存储后端(Posix,S3等)以及回调函数等定制。 8 | 9 | # 调用流程: 10 | 1. 用户首先需要初始化好连接Conn以及存储FS,提供Attribs(可以看作rsync的命令行参数),Receiver支持设置回调函数(下载或过滤特定文件), 11 | 2. 完成握手 12 | 3. 创建Receiver或者Sender,最终调用其Sync方法即可开始rsync协议的自动同步。 13 | 14 | ## 详解: 15 | ```go 16 | type Conn struct { 17 | writer io.WriteCloser // Write only 18 | reader io.ReadCloser // Read only 19 | bytespool []byte // Anti memory-wasted, default size: 8 bytes 20 | } 21 | ``` 22 | 23 | 表示与远程的连接,可以是Socket的TCP连接,SSH连接,标准输出输入等(只要远程支持,理论上还可以rsync over UDP)。Conn的内部读写通道是分开的,这是为了应对rsync在Socket连接时读写不对称问题,比如握手完成后server向client发送的数据是multiple的,而client向server发送却不是。 24 | 25 | Conn提供了rsync协议数据类型的读写支持:byte,short,int,varint变长整数(int64)。 26 | 27 | ```go 28 | type MuxReader struct { 29 | in io.ReadCloser 30 | remain uint32 // Default value: 0 31 | header []byte // Size: 4 bytes 32 | } 33 | ``` 34 | 35 | 36 | MuxReader负责处理multiple消息,剥离消息头,并返回真实的数据。可以通过调用NewMuxReader(reader io.ReadCloser) *MuxReader 完成MuxReader的初始化。 37 | 38 | ```go 39 | type Attribs struct { 40 | Sender bool // --sender 41 | Server bool // --server 42 | Recursive bool // -r 43 | DryRun bool // -n 44 | HasModTime bool // -t 45 | HasPerms bool // -p 46 | HasLinks bool // -l 47 | HasGID bool // -g 48 | HasUID bool // -u 49 | compress bool // -z 未支持 50 | } 51 | ``` 52 | 53 | 类似与rsync的命令行参数,可以控制Receiver/Sender的内部执行流程。 54 | 55 | ```go 56 | type FS interface { 57 | Put(fileName string, content io.Reader, fileSize int64, metadata FileMetadata) (written int64, err error) 58 | Get(fileName string, metadata FileMetadata) (File, error) 59 | Delete(fileName string, mode FileMode) error 60 | List() (FileList, error) 61 | Stats() (seekable bool) 62 | } 63 | ``` 64 | FS是一套存储后端的类型无关的接口,无论是传统的文件存储,或者是对象存储,只要实现这一套接口均可以在rsync同步过程之中作为存储后端使用,不同的系统的差别主要在Seekable,若设置返回false则Sender/Receiver会运行精简后的rsync传输协议(不进行block checksum),否则运行传统的rsync传输协议。 65 | 66 | 67 | **未完成** Callback 这部分接口由用户实现,主要是Rsync的Request以及Delete两个过程回调(Receiver)。 68 | 1. Request:下载文件,回调函数会传入远程的文件列表,用户经过筛选后返回一个int数组(对应文件列表的index)来指示下载的文件。 69 | 2. Delete:删除文件。回调函数会传入即将删除的文件列表,用户决定删除的文件。 70 | 71 | 函数默认会提供这两个过程的函数,对应原版rsync的对应功能,用户调用即可。 72 | 73 | # 工作进度&笔记 74 | ## Sender 75 | 正在实现v27版本的Sender部分,预计实现后可以与rsync的Sender进行文件同步,但不会进行block checksum: 76 | 1. 忽略所有Receiver发送的checksum 77 | 2. 只发送实际的文件内容 78 | 79 | ## 压缩 (对应原版rsync的token.c) 80 | 原版Rsync内部使用了zlib对数据进行压缩,但它zlib相关window bit设置成了-15也就是仅对数据进行deflate或者inflate处例,不会添加额外的包头。 81 | 计划使用go自带的flate包处理这些数据。 82 | 83 | **难题** 原版Rsync使用状态机处理数据,尤其发送压缩的数据时: 84 | 不会发送压缩数据的后4bytes给对方 85 | ```c 86 | /* 87 | * We have to trim off the last 4 88 | * bytes of output when flushing 89 | * (they are just 0, 0, ff, ff). 90 | */ 91 | ``` 92 | 暂时想不出有什么比较好的解决方法。 93 | 94 | 95 | ## Exclusion 96 | 原版rsync v2.6.0是把每个路径以字符串数组存起来的(最新版有重写),发送的时候会去掉文件夹路径结尾的`/` 97 | 98 | # 参考项目 99 | [repositoryd](https://github.com/APNIC-net/repositoryd), An rsync-compatible RPKI repository daemon. -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module rsync-os 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/golang/protobuf v1.4.2 7 | github.com/kaiakz/ubuffer v0.0.0-20200803053910-dd1083087166 8 | github.com/minio/minio-go/v6 v6.0.57 9 | github.com/pkg/errors v0.9.1 10 | github.com/spf13/viper v1.7.1 11 | go.etcd.io/bbolt v1.3.5 12 | golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de 13 | google.golang.org/protobuf v1.23.0 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 7 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 8 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 9 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 10 | cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= 11 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 12 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 13 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 14 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 15 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 16 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 17 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 18 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 19 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 20 | github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= 21 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= 22 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 23 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 24 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 25 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 26 | github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= 27 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 28 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 29 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 30 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 31 | github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 32 | github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 33 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 34 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 35 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 36 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 37 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 38 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 39 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 40 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 41 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 42 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 43 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 44 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 45 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 46 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 47 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 48 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 49 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 50 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 51 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 52 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 53 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 54 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 55 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 56 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 57 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 58 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 59 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 60 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 61 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 62 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 63 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 64 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 65 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 66 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 67 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 68 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 69 | github.com/golang/protobuf v1.4.1 h1:ZFgWrT+bLgsYPirOnRfKLYJLvssAegOj/hgyMFdJZe0= 70 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 71 | github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= 72 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 73 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 74 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 75 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 76 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 77 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 78 | github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= 79 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 80 | github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= 81 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 82 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 83 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 84 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 85 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 86 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 87 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 88 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 89 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 90 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 91 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 92 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 93 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 94 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 95 | github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= 96 | github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= 97 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 98 | github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 99 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 100 | github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= 101 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 102 | github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= 103 | github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= 104 | github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= 105 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 106 | github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 107 | github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= 108 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 109 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 110 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 111 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 112 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= 113 | github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= 114 | github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= 115 | github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= 116 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 117 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 118 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= 119 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 120 | github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= 121 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 122 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 123 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 124 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 125 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 126 | github.com/kaiakz/ubuffer v0.0.0-20200803053910-dd1083087166 h1:IAukUBAVLUWBcexOYgkTD/EjMkfnNos7g7LFpyIdHJI= 127 | github.com/kaiakz/ubuffer v0.0.0-20200803053910-dd1083087166/go.mod h1:T4xUEny5PVedYIbkMAKYEBjMyDsOvvP0qK4s324AKA8= 128 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 129 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 130 | github.com/klauspost/cpuid v1.2.3 h1:CCtW0xUnWGVINKvE/WWOYKdsPV6mawAtvQuSl8guwQs= 131 | github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= 132 | github.com/klauspost/cpuid v1.3.1 h1:5JNjFYYQrZeKRJ0734q51WCEEn2huer72Dc7K+R/b6s= 133 | github.com/klauspost/cpuid v1.3.1/go.mod h1:bYW4mA6ZgKPob1/Dlai2LviZJO7KGI3uoWLd42rAQw4= 134 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 135 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 136 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 137 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 138 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 139 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 140 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 141 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 142 | github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= 143 | github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 144 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 145 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 146 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 147 | github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= 148 | github.com/minio/md5-simd v1.1.0 h1:QPfiOqlZH+Cj9teu0t9b1nTBfPbyTl16Of5MeuShdK4= 149 | github.com/minio/md5-simd v1.1.0/go.mod h1:XpBqgZULrMYD3R+M28PcmP0CkI7PEMzB3U77ZrKZ0Gw= 150 | github.com/minio/minio-go/v6 v6.0.57 h1:ixPkbKkyD7IhnluRgQpGSpHdpvNVaW6OD5R9IAO/9Tw= 151 | github.com/minio/minio-go/v6 v6.0.57/go.mod h1:5+R/nM9Pwrh0vqF+HbYYDQ84wdUFPyXHkrdT4AIkifM= 152 | github.com/minio/sha256-simd v0.1.1 h1:5QHSlgo3nt5yKOJrC7W8w7X+NFl8cMPZm96iu8kKUJU= 153 | github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= 154 | github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= 155 | github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 156 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 157 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 158 | github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= 159 | github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= 160 | github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= 161 | github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 162 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= 163 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 164 | github.com/mitchellh/mapstructure v1.3.3 h1:SzB1nHZ2Xi+17FP0zVQBHIZqvwRN9408fJO8h+eeNA8= 165 | github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 166 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 167 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 168 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 169 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 170 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 171 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 172 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= 173 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 174 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 175 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 176 | github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 177 | github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= 178 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 179 | github.com/pelletier/go-toml v1.8.0 h1:Keo9qb7iRJs2voHvunFtuuYFsbWeOBh8/P9v/kVMFtw= 180 | github.com/pelletier/go-toml v1.8.0/go.mod h1:D6yutnOGMveHEPV7VQOuvI/gXY61bv+9bAOTRnLElKs= 181 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 182 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 183 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 184 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 185 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 186 | github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= 187 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 188 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 189 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 190 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 191 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 192 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 193 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 194 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 195 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 196 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 197 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 198 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 199 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 200 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 201 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 202 | github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 203 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= 204 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 205 | github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo= 206 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= 207 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 208 | github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 209 | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= 210 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 211 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 212 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 213 | github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= 214 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 215 | github.com/spf13/afero v1.3.3 h1:p5gZEKLYoL7wh8VrJesMaYeNxdEd1v3cb4irOk9zB54= 216 | github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= 217 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= 218 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 219 | github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= 220 | github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 221 | github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= 222 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 223 | github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= 224 | github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= 225 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 226 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 227 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 228 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 229 | github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= 230 | github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= 231 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 232 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 233 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 234 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 235 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 236 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 237 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 238 | github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= 239 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 240 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 241 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 242 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 243 | go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0= 244 | go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= 245 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 246 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 247 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 248 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 249 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 250 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 251 | golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 252 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 253 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 254 | golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 255 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 256 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 257 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 258 | golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig= 259 | golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 260 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 261 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 262 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 263 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 264 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 265 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 266 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 267 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 268 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 269 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 270 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 271 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 272 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 273 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 274 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 275 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 276 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 277 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 278 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 279 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 280 | golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 281 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 282 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 283 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 284 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 285 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 286 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 287 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 288 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 289 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 290 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco= 291 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 292 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 293 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= 294 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 295 | golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU= 296 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 297 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 298 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 299 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 300 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 301 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 302 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 303 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 304 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 305 | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 306 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 307 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 308 | golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 309 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 310 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 311 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 312 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 313 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 314 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= 315 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 316 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 317 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 318 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 319 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 320 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 321 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0= 322 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 323 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 324 | golang.org/x/sys v0.0.0-20200802091954-4b90ce9b60b3 h1:qDJKu1y/1SjhWac4BQZjLljqvqiWUhjmDMnonmVGDAU= 325 | golang.org/x/sys v0.0.0-20200802091954-4b90ce9b60b3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 326 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 327 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 328 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 329 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 330 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 331 | golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= 332 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 333 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 334 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 335 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 336 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 337 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 338 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 339 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 340 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 341 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 342 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 343 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 344 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 345 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 346 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 347 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 348 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 349 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 350 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 351 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 352 | golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 353 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 354 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 355 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 356 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 357 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 358 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 359 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 360 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 361 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 362 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 363 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 364 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 365 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 366 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 367 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 368 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 369 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 370 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 371 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 372 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 373 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 374 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 375 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 376 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 377 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 378 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 379 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 380 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 381 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 382 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 383 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 384 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 385 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 386 | google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= 387 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 388 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 389 | google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= 390 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 391 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 392 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 393 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 394 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 395 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 396 | gopkg.in/ini.v1 v1.42.0 h1:7N3gPTt50s8GuLortA00n8AqRTk75qOP98+mTPpgzRk= 397 | gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 398 | gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= 399 | gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 400 | gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww= 401 | gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 402 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 403 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 404 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 405 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 406 | gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= 407 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 408 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 409 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 410 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 411 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 412 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 413 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 414 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 415 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 416 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Send @RSYNCD x.x\n 2 | // Send modname\n 3 | // Send arugment with mod list\0 filter list write(0) \n 4 | // handshake 5 | // batch seed 6 | // Recv file list 7 | // 8 | 9 | package main 10 | 11 | import ( 12 | "flag" 13 | "fmt" 14 | "github.com/spf13/viper" 15 | "log" 16 | "rsync-os/rsync" 17 | "rsync-os/storage" 18 | "time" 19 | ) 20 | 21 | func ClientS3(src string, dest string) { 22 | addr, module, path, err := rsync.SplitURI(src) 23 | 24 | if err != nil { 25 | log.Println("Invaild URI") 26 | return 27 | } 28 | 29 | log.Println(module, path) 30 | 31 | ppath := rsync.TrimPrepath(path) 32 | 33 | if viper.GetStringMapString(dest) == nil { 34 | log.Println("Lack of ", dest) 35 | return 36 | } 37 | 38 | // Config 39 | dbconf := viper.GetStringMapString(dest + ".boltdb") 40 | minioConf := viper.GetStringMapString(dest) 41 | 42 | 43 | stor, _ := storage.NewMinio(module, ppath, dbconf["path"], minioConf["endpoint"], minioConf["keyaccess"], minioConf["keysecret"], false) 44 | defer stor.Close() 45 | 46 | client, err := rsync.SocketClient(stor, addr, module, ppath, nil) 47 | if err != nil { 48 | panic("rsync client fails to initialize") 49 | } 50 | if err := client.Sync(); err != nil { 51 | panic(err) 52 | } 53 | 54 | } 55 | 56 | func main() { 57 | loadConfigIfExists() 58 | flag.Parse() 59 | args := flag.Args() 60 | if len(args) < 2 { 61 | fmt.Println("Usage: rsync-os [OPTION]... rsync://[USER@]HOST[:PORT]/SRC") 62 | return 63 | } 64 | startTime := time.Now() 65 | ClientS3(args[0], args[1]) 66 | log.Println("Duration:", time.Since(startTime)) 67 | } 68 | -------------------------------------------------------------------------------- /rsync/attribs.go: -------------------------------------------------------------------------------- 1 | package rsync 2 | 3 | type Attribs struct { 4 | Sender bool // --sender 5 | Server bool // --server 6 | Recursive bool // -r 7 | DryRun bool // -n 8 | HasModTime bool // -t 9 | HasPerms bool // -p 10 | HasLinks bool // -l 11 | HasGID bool // -g 12 | HasUID bool // -u 13 | //compress bool // -z 14 | } 15 | 16 | func (a *Attribs) Marshal() []byte { 17 | //"--server\n--sender\n-l\n-p\n-r\n-t\n.\n" 18 | args := make([]byte, 0, 64) 19 | if a.Server { 20 | args = append(args, []byte("--server\n")...) 21 | } 22 | if a.Sender { 23 | args = append(args, []byte("--sender\n")...) 24 | } 25 | if a.Recursive { 26 | args = append(args, []byte("-r\n")...) 27 | } 28 | if a.HasModTime { 29 | args = append(args, []byte("-t\n")...) 30 | } 31 | if a.HasLinks { 32 | args = append(args, []byte("-l\n")...) 33 | } 34 | if a.HasPerms { 35 | args = append(args, []byte("-p\n")...) 36 | } 37 | if a.HasGID { 38 | args = append(args, []byte("-g\n")...) 39 | } 40 | if a.HasUID { 41 | args = append(args, []byte("-u\n")...) 42 | } 43 | return args 44 | } 45 | -------------------------------------------------------------------------------- /rsync/chksum.go: -------------------------------------------------------------------------------- 1 | package rsync 2 | 3 | type SumStruct struct { 4 | fileLen uint64 // totol file length 5 | count uint64 // how many chunks 6 | remainder uint64 // fileLen % blockLen 7 | blockLen uint64 // block length 8 | sum2Len uint64 // sum2 length 9 | sumList []SumChunk // chunks 10 | } 11 | 12 | type SumChunk struct { 13 | fileOffset int64 14 | chunkLen uint 15 | sum1 uint32 16 | sum2 []byte 17 | } 18 | 19 | 20 | -------------------------------------------------------------------------------- /rsync/client.go: -------------------------------------------------------------------------------- 1 | package rsync 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "log" 7 | "net" 8 | "strings" 9 | ) 10 | 11 | /* As a Client, we need to: 12 | 1. connect to server by socket or ssh 13 | 2. handshake: version, args, ioerror 14 | PS: client always sends exclusions/filter list 15 | 3. construct a Receiver or a Sender, then excute it. 16 | */ 17 | 18 | // TODO: passes more arguments: cmd 19 | // Connect to rsync daemon 20 | func SocketClient(storage FS, address string, module string, path string, options map[string]string) (SendReceiver, error) { 21 | skt, err := net.Dial("tcp", address) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | conn := &Conn{ 27 | writer: skt, 28 | reader: skt, 29 | bytespool: make([]byte, 8), 30 | } 31 | 32 | /* HandShake by socket */ 33 | // send my version 34 | _, err = conn.Write([]byte(RSYNC_VERSION)) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | // receive server's protocol version and seed 40 | versionStr, _ := readLine(conn) 41 | 42 | // recv(version) 43 | var remoteProtocol, remoteProtocolSub int 44 | _, err = fmt.Sscanf(versionStr, "@RSYNCD: %d.%d", remoteProtocol, remoteProtocolSub) 45 | if err != nil { 46 | // FIXME: (panic)type not a pointer: int 47 | //panic(err) 48 | } 49 | log.Println(versionStr) 50 | 51 | buf := new(bytes.Buffer) 52 | 53 | // send mod name 54 | buf.WriteString(module) 55 | buf.WriteByte('\n') 56 | _, err = conn.Write(buf.Bytes()) 57 | if err != nil { 58 | return nil, err 59 | } 60 | buf.Reset() 61 | 62 | // Wait for '@RSYNCD: OK' 63 | for { 64 | res, err := readLine(conn) 65 | if err != nil { 66 | return nil, err 67 | } 68 | log.Print(res) 69 | if strings.Contains(res, RSYNCD_OK) { 70 | break 71 | } 72 | } 73 | 74 | // Send arguments 75 | buf.Write([]byte(SAMPLE_ARGS)) 76 | buf.Write([]byte(module)) 77 | buf.Write([]byte(path)) 78 | buf.Write([]byte("\n\n")) 79 | _, err = conn.Write(buf.Bytes()) 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | // read int32 as seed 85 | seed, err := conn.ReadInt() 86 | if err != nil { 87 | return nil, err 88 | } 89 | log.Println("SEED", seed) 90 | 91 | // HandShake OK 92 | log.Println("Handshake completed") 93 | 94 | // Begin to demux 95 | conn.reader = NewMuxReader(conn.reader) 96 | 97 | // As a client, we need to send filter list 98 | err = conn.WriteInt(EXCLUSION_END) 99 | if err != nil { 100 | return nil, err 101 | } 102 | 103 | // TODO: Sender 104 | 105 | return &Receiver{ 106 | conn: conn, 107 | module: module, 108 | path: path, 109 | seed: seed, 110 | storage: storage, 111 | }, nil 112 | } 113 | 114 | // Connect to sshd, and start a rsync server on remote 115 | func SshClient(storage FS, address string, module string, path string, options map[string]string) (SendReceiver, error) { 116 | // TODO: build args 117 | 118 | ssh, err := NewSSH(address, "", "", "rsync --server --sender -l -p -r -t") 119 | if err != nil { 120 | return nil, err 121 | } 122 | conn := &Conn{ 123 | writer: ssh, 124 | reader: ssh, 125 | bytespool: make([]byte, 8), 126 | } 127 | 128 | // Handshake 129 | lver, err := conn.ReadInt() 130 | if err != nil { 131 | return nil, err 132 | } 133 | 134 | rver, err := conn.ReadInt() 135 | if err != nil { 136 | return nil, err 137 | } 138 | 139 | seed, err := conn.ReadInt() 140 | if err != nil { 141 | return nil, err 142 | } 143 | 144 | // TODO: Sender 145 | 146 | return &Receiver{ 147 | conn: conn, 148 | module: module, 149 | path: path, 150 | seed: seed, 151 | lVer: lver, 152 | rVer: rver, 153 | storage: storage, 154 | }, nil 155 | } 156 | -------------------------------------------------------------------------------- /rsync/compress.go: -------------------------------------------------------------------------------- 1 | package rsync 2 | 3 | import ( 4 | "compress/flate" 5 | "io" 6 | ) 7 | 8 | /* 9 | rsync uses zlib to do compression, the windowsBits is -15: raw data 10 | */ 11 | 12 | const ( 13 | END_FLAG = 0 14 | TOKEN_LONG = 0x20 15 | TOKENRUN_LONG = 0x21 16 | DEFLATED_DATA = 0x40 17 | TOKEN_REL = 0x80 18 | TOKENRUN_REL = 0xc0 19 | 20 | ) 21 | 22 | // RFC 1951: https://tools.ietf.org/html/rfc1951 23 | type flatedtokenReader struct { 24 | in Conn 25 | flatedwraper *flatedWraper 26 | decompressor io.ReadCloser 27 | savedflag byte 28 | flag byte 29 | remains uint32 30 | } 31 | 32 | 33 | 34 | func NewflatedtokenReader(reader Conn) *flatedtokenReader { 35 | w := &flatedWraper{ 36 | raw: &reader, 37 | end: [4]byte{0, 0, 0xff, 0xff}, 38 | } 39 | return &flatedtokenReader{ 40 | in: reader, 41 | flatedwraper: w, 42 | decompressor: flate.NewReader(w), 43 | savedflag: -1, 44 | flag: 0, 45 | remains: 0, 46 | } 47 | } 48 | 49 | // Update flag & len of remain data 50 | func (f *flatedtokenReader) readFlag() error { 51 | if f.savedflag != 0 { 52 | f.flag = f.savedflag & 0xff 53 | f.savedflag = 0 54 | } else { 55 | var err error 56 | if f.flag, err = f.in.ReadByte(); err != nil { 57 | return err 58 | } 59 | } 60 | if (f.flag & 0xc0) == DEFLATED_DATA { 61 | l, err := f.in.ReadByte() 62 | if err != nil { 63 | return err 64 | } 65 | f.remains = uint32(f.flag & 0x3f) << 8 + uint32(l) 66 | } 67 | return nil 68 | } 69 | 70 | func (f *flatedtokenReader) Read(p []byte) (n int, err error) { 71 | n, err = f.decompressor.Read(p) 72 | f.remains -= uint32(n) 73 | return 74 | } 75 | 76 | func (f *flatedtokenReader) Close() error { 77 | return f.decompressor.Close() 78 | } 79 | 80 | // Hack only: rsync need to append 4 bytes(0, 0, ff, ff) at the end. 81 | type flatedWraper struct { 82 | raw io.Reader 83 | end [4]byte 84 | } 85 | 86 | func (f *flatedWraper) Read(p []byte) (n int, err error) { 87 | // Just append 4 bytes to the end of stream 88 | return f.raw.Read(p) 89 | } -------------------------------------------------------------------------------- /rsync/connection.go: -------------------------------------------------------------------------------- 1 | package rsync 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "io" 7 | ) 8 | 9 | type SendReceiver interface { 10 | Sync() error 11 | } 12 | 13 | // io.ReadWriteCloser 14 | // This struct has two main attributes, both of them can be used for a plain socket or an SSH 15 | type Conn struct { 16 | writer io.WriteCloser // Write only 17 | reader io.ReadCloser // Read only 18 | bytespool []byte // Anti memory-wasted, default size: 8 bytes 19 | } 20 | 21 | func (conn *Conn) Write(p []byte) (n int, err error) { 22 | return conn.writer.Write(p) 23 | } 24 | 25 | func (conn *Conn) Read(p []byte) (n int, err error) { 26 | return conn.reader.Read(p) 27 | } 28 | 29 | /* Encoding: little endian */ 30 | // size of: int: 4, long: 8, varint: 4 or 8 31 | func (conn *Conn) ReadByte() (byte, error) { 32 | val := conn.bytespool[:1] 33 | _, err := io.ReadFull(conn, val) 34 | if err != nil { 35 | return 0, err 36 | } 37 | return conn.bytespool[0], nil 38 | } 39 | 40 | func (conn *Conn) ReadShort() (int16, error) { 41 | val := conn.bytespool[:2] 42 | _, err := io.ReadFull(conn, val) 43 | if err != nil { 44 | return 0, err 45 | } 46 | return int16(binary.LittleEndian.Uint16(val)), nil 47 | } 48 | 49 | func (conn *Conn) ReadInt() (int32, error) { 50 | val := conn.bytespool[:4] 51 | _, err := io.ReadFull(conn, val) 52 | if err != nil { 53 | return 0, err 54 | } 55 | return int32(binary.LittleEndian.Uint32(val)), nil 56 | } 57 | 58 | func (conn *Conn) ReadLong() (int64, error) { 59 | val := conn.bytespool[:8] 60 | _, err := io.ReadFull(conn, val) 61 | if err != nil { 62 | return 0, err 63 | } 64 | return int64(binary.LittleEndian.Uint64(val)), nil 65 | } 66 | 67 | func (conn *Conn) ReadVarint() (int64, error) { 68 | sval, err := conn.ReadInt(); 69 | if err != nil { 70 | return 0, err 71 | } 72 | if sval != -1 { 73 | return int64(sval), nil 74 | } 75 | return conn.ReadLong() 76 | } 77 | 78 | func (conn *Conn) WriteByte(data byte) error { 79 | return binary.Write(conn.writer, binary.LittleEndian, data) 80 | } 81 | 82 | func (conn *Conn) WriteShort(data int16) error { 83 | return binary.Write(conn.writer, binary.LittleEndian, data) 84 | } 85 | 86 | func (conn *Conn) WriteInt(data int32) error { 87 | return binary.Write(conn.writer, binary.LittleEndian, data) 88 | } 89 | 90 | func (conn *Conn) WriteLong(data int64) error { 91 | return binary.Write(conn.writer, binary.LittleEndian, data) 92 | } 93 | 94 | // TODO: If both writer and reader are based on a same Connection (socket, SSH), how to close them twice? 95 | func (conn *Conn) Close() error { 96 | _ = conn.writer.Close() 97 | _ = conn.reader.Close() 98 | return nil 99 | } 100 | 101 | func readLine(conn *Conn) (string, error) { 102 | // until \n, then add \0 103 | line := new(bytes.Buffer) 104 | for { 105 | c, err := conn.ReadByte() 106 | if err != nil { 107 | return "", err 108 | } 109 | 110 | if c == '\r' { 111 | continue 112 | } 113 | 114 | err = line.WriteByte(c) 115 | if err != nil { 116 | return "", err 117 | } 118 | 119 | if c == '\n' { 120 | line.WriteByte(0) 121 | break 122 | } 123 | 124 | if c == 0 { 125 | break 126 | } 127 | } 128 | return line.String(), nil 129 | } -------------------------------------------------------------------------------- /rsync/const.go: -------------------------------------------------------------------------------- 1 | package rsync 2 | 3 | const ( 4 | RSYNC_VERSION = "@RSYNCD: 27.0\n" 5 | RSYNCD_OK = "@RSYNCD: OK" 6 | RSYNC_EXIT = "@RSYNCD: EXIT" 7 | 8 | K = 1 << 10 9 | M = 1 << 20 10 | G = 1 << 30 11 | 12 | MAXPATHLEN = 1024 13 | INDEX_END = int32(-1) 14 | EXCLUSION_END = int32(0) 15 | END1 = '\n' 16 | END2 = '\x00' 17 | PHASE_END = int32(-1) 18 | 19 | // ARGUMENTS 20 | ARG_SERVER = "--server" 21 | ARG_SENDER = "--sender" 22 | ARG_SYMLINK = "-l" 23 | ARG_RECURSIVE = "-r" 24 | ARG_PERMS = "-p" 25 | SAMPLE_ARGS = "--server\n--sender\n-l\n-p\n-r\n-t\n.\n" 26 | SAMPLE_LIST_ARGS = "--server\n--sender\n--list-only\n-l\n-p\n-r\n-t\n.\n" 27 | 28 | // For Multiplex(1 byte) 29 | MUX_BASE = 7 30 | MSG_DATA = 0 31 | MSG_ERROR_XFER = 1 32 | MSG_INFO = 2 33 | MSG_ERROR = 3 34 | MSG_WARNING = 4 35 | MSG_IO_ERROR = 22 36 | MSG_NOOP = 42 37 | MSG_SUCCESS = 100 38 | MSG_DELETED = 101 39 | MSG_NO_SEND = 102 40 | 41 | // For FILE LIST(1 byte) 42 | FLIST_END = 0x00 43 | FLIST_TOP_LEVEL = 0x01 /* needed for remote --delete */ 44 | FLIST_MODE_SAME = 0x02 /* mode is repeat */ 45 | FLIST_RDEV_SAME = 0x04 /* rdev is repeat */ 46 | FLIST_UID_SAME = 0x08 /* uid is repeat */ 47 | FLIST_GID_SAME = 0x10 /* gid is repeat */ 48 | FLIST_NAME_SAME = 0x20 /* name is repeat */ 49 | FLIST_NAME_LONG = 0x40 /* name >255 bytes */ 50 | FLIST_TIME_SAME = 0x80 /* time is repeat */ 51 | 52 | // File type 53 | S_IFMT = 0170000 /* Type of file */ 54 | S_IFREG = 0100000 /* Regular file. */ 55 | S_IFDIR = 0040000 /* Directory. */ 56 | S_IFLNK = 0120000 /* Symbolic link. */ 57 | S_IFCHR = 0020000 /* Character device. */ 58 | S_IFBLK = 0060000 /* Block device. */ 59 | S_IFIFO = 0010000 /* FIFO. */ 60 | S_IFSOCK = 0140000 /* Socket. */ 61 | ) 62 | -------------------------------------------------------------------------------- /rsync/demux.go: -------------------------------------------------------------------------------- 1 | package rsync 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "io" 7 | "log" 8 | ) 9 | 10 | //Multiplexing 11 | //Most rsync transmissions are wrapped in a multiplexing envelope protocol. It is 12 | //composed as follows: 13 | // 14 | //1. envelope header (4 bytes) 15 | //2. envelope payload (arbitrary length) 16 | // 17 | //The first byte of the envelope header consists of a tag. If the tag is 7, the pay‐ 18 | //load is normal data. Otherwise, the payload is out-of-band server messages. If the 19 | //tag is 1, it is an error on the sender's part and must trigger an exit. This limits 20 | //message payloads to 24 bit integer size, 0x00ffffff. 21 | // 22 | //The only data not using this envelope are the initial handshake between client and 23 | //server 24 | 25 | type MuxReader struct { 26 | in io.ReadCloser 27 | remain uint32 // Default value: 0 28 | header []byte // Size: 4 bytes 29 | } 30 | 31 | func NewMuxReader(reader io.ReadCloser) *MuxReader { 32 | return &MuxReader{ 33 | in: reader, 34 | remain: 0, 35 | header: make([]byte, 4), 36 | } 37 | } 38 | 39 | func (r *MuxReader) Read(p []byte) (n int, err error) { 40 | if r.remain == 0 { 41 | err := r.readHeader() 42 | if err != nil { 43 | return 0, err 44 | } 45 | } 46 | rlen := uint32(len(p)) 47 | if rlen > r.remain { // Min(len(p), remain) 48 | rlen = r.remain 49 | } 50 | n, err = r.in.Read(p[:rlen]) 51 | r.remain = r.remain - uint32(n) 52 | return 53 | } 54 | 55 | func (r *MuxReader) readHeader() error { 56 | for { 57 | // Read header 58 | if _, err := io.ReadFull(r.in, r.header); err != nil { 59 | return err 60 | } 61 | tag := r.header[3] // Little Endian 62 | size := (binary.LittleEndian.Uint32(r.header) & 0xffffff) // TODO: zero? 63 | 64 | log.Printf(" tag %d size %d\n", tag, size) 65 | 66 | if tag == (MUX_BASE + MSG_DATA) { // MUX_BASE + MSG_DATA 67 | r.remain = size 68 | return nil 69 | } else { // out-of-band data 70 | // otag := tag - 7 71 | msg := make([]byte, size) 72 | if _, err := io.ReadFull(r.in, msg); err != nil { 73 | return err 74 | } 75 | return errors.New(string(msg)) 76 | } 77 | } 78 | } 79 | 80 | func (r *MuxReader) Close() error { 81 | return r.in.Close() 82 | } 83 | 84 | -------------------------------------------------------------------------------- /rsync/demux_old.go: -------------------------------------------------------------------------------- 1 | package rsync 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | "log" 7 | ) 8 | 9 | //Multiplexing 10 | //Most rsync transmissions are wrapped in a multiplexing envelope protocol. It is 11 | //composed as follows: 12 | // 13 | //1. envelope header (4 bytes) 14 | //2. envelope payload (arbitrary length) 15 | // 16 | //The first byte of the envelope header consists of a tag. If the tag is 7, the pay‐ 17 | //load is normal data. Otherwise, the payload is out-of-band server messages. If the 18 | //tag is 1, it is an error on the sender's part and must trigger an exit. This limits 19 | //message payloads to 24 bit integer size, 0x00ffffff. 20 | // 21 | //The only data not using this envelope are the initial handshake between client and 22 | //server 23 | 24 | type MuxReaderV0 struct { 25 | in io.ReadCloser 26 | Data chan byte 27 | closeCh chan byte 28 | } 29 | 30 | func NewMuxReaderV0(reader io.ReadCloser) *MuxReaderV0 { 31 | mr := &MuxReaderV0{ 32 | in: reader, 33 | Data: make(chan byte, 16 * M), 34 | closeCh: make(chan byte), 35 | } 36 | // Demux in Goroutine 37 | go func() { 38 | 39 | header := make([]byte, 4) // Header size: 4 bytes 40 | var dsize uint32 = 1 << 16 // Default size: 65536 41 | bytespool := make([]byte, dsize) 42 | 43 | for { 44 | select { 45 | case <-mr.closeCh: // Close the channel, then exit the goroutine 46 | close(mr.Data) 47 | return 48 | default: 49 | // read the multipex data & put them to channel 50 | _, err := io.ReadFull(reader, header) 51 | if err != nil { 52 | panic("Multiplex: wire protocol error") 53 | } 54 | 55 | tag := header[3] // Little Endian 56 | size := (binary.LittleEndian.Uint32(header) & 0xffffff) // TODO: zero? 57 | 58 | log.Printf(" tag %d size %d\n", tag, size) 59 | 60 | if tag == (MUX_BASE + MSG_DATA) { // MUX_BASE + MSG_DATA 61 | if size > dsize { 62 | bytespool = make([]byte, size) 63 | dsize = size 64 | } 65 | 66 | body := bytespool[:size] 67 | _, err := io.ReadFull(reader, body) 68 | 69 | // FIXME: Never return EOF 70 | if err != nil { // The connection was closed by server 71 | panic(err) 72 | } 73 | 74 | for _, b := range body { 75 | mr.Data <- b 76 | } 77 | 78 | } else { // out-of-band data 79 | //otag := tag - 7 80 | msg := make([]byte, size) 81 | if _, err := io.ReadFull(reader, msg); err != nil { 82 | panic("Failed to read out-of-band data") 83 | } 84 | panic("out-of-band data: " + string(msg)) 85 | } 86 | } 87 | } 88 | }() 89 | return mr 90 | } 91 | 92 | // FIXME: Never return error 93 | func (r *MuxReaderV0) Read(p []byte) (n int, err error) { 94 | for i, _ := range p { 95 | p[i] = <- r.Data 96 | } 97 | return len(p), nil 98 | } 99 | 100 | func (r *MuxReaderV0) Close() error { 101 | r.closeCh <- 0 // close the channel Data & exit the demux goroutine 102 | return r.in.Close() 103 | } 104 | -------------------------------------------------------------------------------- /rsync/exclude.go: -------------------------------------------------------------------------------- 1 | package rsync 2 | 3 | import ( 4 | "path/filepath" 5 | "strings" 6 | ) 7 | 8 | // Reference: rsync 2.6.0 9 | // --exclude & --exclude-from 10 | 11 | /* These default ignored items come from the CVS manual. 12 | * "RCS SCCS CVS CVS.adm RCSLOG cvslog.* tags TAGS" 13 | * " .make.state .nse_depinfo *~ #* .#* ,* _$* *$" 14 | * " *.old *.bak *.BAK *.orig *.rej .del-*" 15 | * " *.a *.olb *.o *.obj *.so *.exe" 16 | * " *.Z *.elc *.ln core" 17 | The rest we added to suit ourself. 18 | * " .svn/ .bzr/" 19 | */ 20 | 21 | // Filter List 22 | type Exclusion struct { 23 | patterns []string 24 | root string 25 | } 26 | 27 | func (e *Exclusion) Match(name string) (matched bool, err error) { 28 | matched = false 29 | for _, p := range e.patterns { 30 | if strings.HasPrefix(name, p) && name[len(p)] == '/' { 31 | return true, nil 32 | } 33 | if matched, err = filepath.Match(p, name); matched || err != nil {break} 34 | } 35 | return 36 | } 37 | 38 | func (e *Exclusion) Add(pattern string) { 39 | // Check the root, if not empty, join them 40 | e.patterns = append(e.patterns, filepath.Join(e.root, pattern)) 41 | } 42 | 43 | // This is only called by the client 44 | func (e *Exclusion) SendExlusion(conn Conn) error { 45 | // If list_only && !recurse, add '/*/*' 46 | 47 | 48 | // For each item, send its length first 49 | for _, p := range e.patterns { 50 | plen := int32(len(p)) 51 | // TODO: If a dir, append a '/' at the end 52 | if err := conn.WriteInt(plen); err != nil { 53 | return err 54 | } 55 | if _, err := conn.Write([]byte(p)); err != nil { 56 | return err 57 | } 58 | } 59 | 60 | if err := conn.WriteInt(EXCLUSION_END); err != nil { 61 | return err 62 | } 63 | return nil 64 | } 65 | 66 | -------------------------------------------------------------------------------- /rsync/flist.go: -------------------------------------------------------------------------------- 1 | package rsync 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | ) 7 | 8 | type FileInfo struct { 9 | Path []byte 10 | Size int64 11 | Mtime int32 12 | Mode FileMode 13 | } 14 | 15 | // For unix/linux 16 | type FileMode uint32 17 | 18 | // reference: os/types.go 19 | func NewFileMode(mode os.FileMode) FileMode { 20 | m := FileMode(mode & 0777) 21 | // TODO: supports symlink 22 | if mode.IsRegular() { 23 | return m | S_IFREG 24 | } 25 | 26 | types := map[uint32]FileMode { 27 | 0: S_IFDIR, 28 | 4: S_IFLNK, 29 | 5: S_IFBLK, 30 | 6: S_IFIFO, 31 | 7: S_IFSOCK, 32 | 10: S_IFCHR, 33 | } 34 | for i, t := range types { 35 | if m&(1< b. 169 | // Compare their paths by bytes.Compare 170 | // The result will be 0 if a==b, -1 if a < b, and +1 if a > b 171 | // If 1, B doesn't have 172 | // If 0, A & B have 173 | // If -1, A doesn't have 174 | switch bytes.Compare(L[i].Path, R[j].Path) { 175 | case 0: 176 | if L[i].Mtime != R[j].Mtime || L[i].Size != R[j].Size { 177 | newitems = append(newitems, j) 178 | } 179 | i++ 180 | j++ 181 | break 182 | case 1: 183 | newitems = append(newitems, j) 184 | j++ 185 | break 186 | case -1: 187 | olditems = append(olditems, i) 188 | i++ 189 | break 190 | } 191 | } 192 | 193 | // Handle remains 194 | for ; i < len(L); i++ { 195 | olditems = append(olditems, i) 196 | } 197 | for ; j < len(R); j++ { 198 | newitems = append(newitems, j) 199 | } 200 | 201 | return 202 | } 203 | -------------------------------------------------------------------------------- /rsync/fs.go: -------------------------------------------------------------------------------- 1 | package rsync 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | type FileMetadata struct { 8 | Mtime int32 9 | Mode FileMode 10 | } 11 | 12 | // File System: need to handle all type of files: regular, folder, symlink, etc 13 | type FS interface { 14 | Put(fileName string, content io.Reader, fileSize int64, metadata FileMetadata) (written int64, err error) 15 | //Get(fileName string, metadata FileMetadata) (File, error) 16 | Delete(fileName string, mode FileMode) error 17 | List() (FileList, error) 18 | //Stats() (seekable bool) 19 | } 20 | 21 | // Interface: Read, ReadAt, Seek, Close 22 | type File interface { 23 | io.Reader 24 | io.ReaderAt 25 | io.Seeker 26 | io.Closer 27 | } -------------------------------------------------------------------------------- /rsync/receiver.go: -------------------------------------------------------------------------------- 1 | package rsync 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "github.com/kaiakz/ubuffer" 9 | "io" 10 | "log" 11 | "sort" 12 | "time" 13 | ) 14 | 15 | /* Receiver: 16 | 1. Receive File list 17 | 2. Request files by sending files' index 18 | 3. Receive Files, pass the files to storage 19 | */ 20 | type Receiver struct { 21 | conn *Conn 22 | module string 23 | path string 24 | seed int32 25 | lVer int32 26 | rVer int32 27 | storage FS 28 | } 29 | 30 | func (r *Receiver) BuildArgs() string { 31 | return "" 32 | } 33 | 34 | // DeMux was started here 35 | func (r *Receiver) StartMuxIn() { 36 | r.conn.reader = NewMuxReaderV0(r.conn.reader) 37 | } 38 | 39 | func (r *Receiver) SendExclusions() error { 40 | // Send exclusion 41 | return r.conn.WriteInt(EXCLUSION_END) 42 | } 43 | 44 | // Return a filelist from remote 45 | func (r *Receiver) RecvFileList() (FileList, map[string][]byte, error) { 46 | filelist := make(FileList, 0, 1 *M) 47 | symlinks := make(map[string][]byte) 48 | for { 49 | flags, err := r.conn.ReadByte() 50 | if err != nil { 51 | return filelist, symlinks, err 52 | } 53 | 54 | if flags == FLIST_END { 55 | break 56 | } 57 | //fmt.Printf("[%d]\n", flags) 58 | 59 | lastIndex := len(filelist) - 1 60 | var partial, pathlen uint32 = 0, 0 61 | 62 | /* 63 | * Read our filename. 64 | * If we have FLIST_NAME_SAME, we inherit some of the last 65 | * transmitted name. 66 | * If we have FLIST_NAME_LONG, then the string length is greater 67 | * than byte-size. 68 | */ 69 | if (flags & FLIST_NAME_SAME) != 0 { 70 | val, err := r.conn.ReadByte() 71 | if err != nil { 72 | return filelist, symlinks, err 73 | } 74 | partial = uint32(val) 75 | //fmt.Println("Partical", partial) 76 | } 77 | 78 | /* Get the (possibly-remaining) filename length. */ 79 | if (flags & FLIST_NAME_LONG) != 0 { 80 | val, err := r.conn.ReadInt() 81 | if err != nil { 82 | return filelist, symlinks, err 83 | } 84 | pathlen = uint32(val) // can't use for rsync 31 85 | 86 | } else { 87 | val, err := r.conn.ReadByte() 88 | if err != nil { 89 | return filelist, symlinks, err 90 | } 91 | pathlen = uint32(val) 92 | } 93 | //fmt.Println("PathLen", pathlen) 94 | 95 | /* Allocate our full filename length. */ 96 | /* FIXME: maximum pathname length. */ 97 | // TODO: if pathlen + partical == 0 98 | // malloc len error? 99 | 100 | p := make([]byte, pathlen) 101 | _, err = io.ReadFull(r.conn, p) 102 | if err != nil { 103 | return filelist, symlinks, err 104 | } 105 | 106 | path := make([]byte, 0, partial + pathlen) 107 | /* If so, use last */ 108 | if (flags & FLIST_NAME_SAME) != 0 { // FLIST_NAME_SAME 109 | last := filelist[lastIndex] 110 | path = append(path, last.Path[0:partial]...) 111 | } 112 | path = append(path, p...) 113 | //fmt.Println("Path ", string(path)) 114 | 115 | size, err := r.conn.ReadVarint() 116 | if err != nil { 117 | return filelist, symlinks, err 118 | } 119 | //fmt.Println("Size ", size) 120 | 121 | /* Read the modification time. */ 122 | var mtime int32 123 | if (flags & FLIST_TIME_SAME) == 0 { 124 | mtime, err = r.conn.ReadInt() 125 | if err != nil { 126 | return filelist, symlinks, err 127 | } 128 | } else { 129 | mtime = filelist[lastIndex].Mtime 130 | } 131 | //fmt.Println("MTIME ", mtime) 132 | 133 | /* Read the file mode. */ 134 | var mode FileMode 135 | if (flags & FLIST_MODE_SAME) == 0 { 136 | val, err := r.conn.ReadInt() 137 | if err != nil { 138 | return filelist, symlinks, err 139 | } 140 | mode = FileMode(val) 141 | } else { 142 | mode = filelist[lastIndex].Mode 143 | } 144 | //fmt.Println("Mode", uint32(mode)) 145 | 146 | // TODO: Sym link 147 | if ((mode & 32768) != 0) && ((mode & 8192) != 0) { 148 | sllen, err := r.conn.ReadInt() 149 | if err != nil { 150 | return filelist, symlinks, err 151 | } 152 | slink := make([]byte, sllen) 153 | _, err = io.ReadFull(r.conn, slink) 154 | symlinks[string(path)] = slink 155 | if err != nil { 156 | return filelist, symlinks, errors.New("Failed to read symlink") 157 | } 158 | //fmt.Println("Symbolic Len:", len, "Content:", slink) 159 | } 160 | 161 | fmt.Println("@", string(path), mode, size, mtime) 162 | 163 | filelist = append(filelist, FileInfo{ 164 | Path: path, 165 | Size: size, 166 | Mtime: mtime, 167 | Mode: mode, 168 | }) 169 | } 170 | 171 | // Sort the filelist lexicographically 172 | sort.Sort(filelist) 173 | 174 | return filelist, symlinks, nil 175 | } 176 | 177 | // Generator: handle files: if it's a regular file, send its index. Otherwise, put them to storage 178 | func (r *Receiver) Generator(remoteList FileList, downloadList []int, symlinks map[string][]byte) error { 179 | emptyBlocks := make([]byte, 16) // 4 + 4 + 4 + 4 bytes, all bytes set to 0 180 | content := new(bytes.Buffer) 181 | 182 | for _, v := range downloadList { 183 | if remoteList[v].Mode.IsREG() { 184 | if err := r.conn.WriteInt(int32(v)); err != nil { 185 | log.Println("Failed to send index") 186 | return err 187 | } 188 | //fmt.Println("Request: ", string(remoteList[v].Path), uint32(remoteList[v].Mode)) 189 | if _, err := r.conn.Write(emptyBlocks); err != nil { 190 | return err 191 | } 192 | } else { 193 | // TODO: Supports more file mode 194 | // EXPERIMENTAL 195 | // Handle folders & symbol links 196 | content.Reset() 197 | size := remoteList[v].Size 198 | if remoteList[v].Mode.IsLNK() { 199 | if _, err := content.Write(symlinks[string(remoteList[v].Path)]); err != nil { 200 | return err 201 | } 202 | size = int64(content.Len()) 203 | } 204 | 205 | if _, err := r.storage.Put(string(remoteList[v].Path), content, size, FileMetadata{ 206 | Mtime: remoteList[v].Mtime, 207 | Mode: remoteList[v].Mode, 208 | }); err != nil { 209 | return err 210 | } 211 | } 212 | } 213 | 214 | // Send -1 to finish, then start to download 215 | if err := r.conn.WriteInt(INDEX_END); err != nil { 216 | log.Println("Can't send INDEX_END") 217 | return err 218 | } 219 | log.Println("Request completed") 220 | 221 | startTime := time.Now() 222 | err := r.FileDownloader(remoteList[:]) 223 | log.Println("Downloaded duration:", time.Since(startTime)) 224 | return err 225 | } 226 | 227 | // TODO: It is better to update files in goroutine 228 | func (r *Receiver) FileDownloader(localList FileList) error { 229 | 230 | rmd4 := make([]byte, 16) 231 | 232 | for { 233 | index, err := r.conn.ReadInt() 234 | if err != nil { 235 | return err 236 | } 237 | if index == INDEX_END { // -1 means the end of transfer files 238 | return nil 239 | } 240 | //fmt.Println("INDEX:", index) 241 | 242 | count, err := r.conn.ReadInt() /* block count */ 243 | if err != nil { 244 | return err 245 | } 246 | 247 | blen, err := r.conn.ReadInt() /* block length */ 248 | if err != nil { 249 | return err 250 | } 251 | 252 | clen, err := r.conn.ReadInt() /* checksum length */ 253 | if err != nil { 254 | return err 255 | } 256 | 257 | remainder, err := r.conn.ReadInt() /* block remainder */ 258 | if err != nil { 259 | return err 260 | } 261 | 262 | path := localList[index].Path 263 | log.Println("Downloading:", string(path), count, blen, clen, remainder, localList[index].Size) 264 | 265 | // If the file is too big to store in memory, creates a temporary file in the directory 'tmp' 266 | buffer := ubuffer.NewBuffer(localList[index].Size) 267 | downloadeSize := 0 268 | bufwriter := bufio.NewWriter(buffer) 269 | 270 | // Create MD4 271 | //lmd4 := md4.New() 272 | //if err := binary.Write(lmd4, binary.LittleEndian, r.seed); err != nil { 273 | // log.Println("Failed to compute md4") 274 | //} 275 | 276 | for { 277 | token, err := r.conn.ReadInt() 278 | if err != nil { 279 | return err 280 | } 281 | log.Println("TOKEN", token) 282 | if token == 0 { 283 | break 284 | } else if token < 0 { 285 | return errors.New("Does not support block checksum") 286 | // Reference 287 | } else { 288 | ctx := make([]byte, token) // FIXME: memory leak? 289 | _, err = io.ReadFull(r.conn, ctx) 290 | if err != nil { 291 | return err 292 | } 293 | downloadeSize += int(token) 294 | log.Println("Downloaded:", downloadeSize, "byte") 295 | if _, err := bufwriter.Write(ctx); err != nil { 296 | return err 297 | } 298 | //if _, err := lmd4.Write(ctx); err != nil { 299 | // return err 300 | //} 301 | } 302 | } 303 | if bufwriter.Flush() != nil { 304 | return errors.New("Failed to flush buffer") 305 | } 306 | 307 | // Remote MD4 308 | // TODO: compare computed MD4 with remote MD4 309 | _, err = io.ReadFull(r.conn, rmd4) 310 | if err != nil { 311 | return err 312 | } 313 | // Compare two MD4 314 | //if bytes.Compare(rmd4, lmd4.Sum(nil)) != 0 { 315 | // log.Println("Checksum error") 316 | //} 317 | 318 | // Put file to object storage 319 | var n int64 320 | n, err = buffer.Seek(0, io.SeekStart) 321 | 322 | n, err = r.storage.Put(string(path), buffer, int64(downloadeSize), FileMetadata{ 323 | Mtime: localList[index].Mtime, 324 | Mode: localList[index].Mode, 325 | }) 326 | if err != nil { 327 | return err 328 | } 329 | 330 | if buffer.Finalize() != nil { 331 | return errors.New("Buffer can't be finalized") 332 | } 333 | 334 | log.Printf("Successfully uploaded %s of size %d\n", path, n) 335 | } 336 | } 337 | 338 | // Clean up local files 339 | func (r *Receiver) FileCleaner(localList FileList, deleteList []int) error { 340 | // Since file list was already sorted, we can iterate it in the reverse direction to traverse the file tree in post-order 341 | // Thus it always cleans sub-files firstly 342 | for i := len(deleteList) - 1; i >= 0; i-- { 343 | fname := string(localList[deleteList[i]].Path) 344 | err := r.storage.Delete(fname, localList[deleteList[i]].Mode) 345 | log.Println("Deleted:", fname) 346 | if err != nil { 347 | return err 348 | } 349 | } 350 | return nil 351 | } 352 | 353 | func (r *Receiver) FinalPhase() error { 354 | //go func() { 355 | // ioerror, err := r.conn.ReadInt() 356 | // log.Println(ioerror, err) 357 | //}() 358 | 359 | err := r.conn.WriteInt(INDEX_END) 360 | if err != nil { 361 | return err 362 | } 363 | return r.conn.WriteInt(INDEX_END) 364 | } 365 | 366 | func (r *Receiver) Sync() error { 367 | defer func() { 368 | log.Println("Task completed", r.conn.Close()) // TODO: How to handle errors from Close 369 | }() 370 | 371 | lfiles, err := r.storage.List() 372 | if err != nil { 373 | return err 374 | } 375 | //for _, v := range lfiles { 376 | // fmt.Println("Local File:", string(v.Path), v.Mode, v.Mtime) 377 | //} 378 | 379 | rfiles, symlinks, err := r.RecvFileList() 380 | if err != nil { 381 | return err 382 | } 383 | log.Println("Remote files count:", len(rfiles)) 384 | 385 | ioerr, err := r.conn.ReadInt() 386 | if err != nil { 387 | return nil 388 | } 389 | log.Println("IOERR", ioerr) 390 | 391 | newfiles, oldfiles := lfiles.Diff(rfiles) 392 | if len(newfiles) == 0 && len(oldfiles) == 0 { 393 | log.Println("There is nothing to do") 394 | } 395 | fmt.Print(newfiles, oldfiles) 396 | 397 | if err := r.Generator(rfiles[:], newfiles[:], symlinks); err != nil { 398 | return err 399 | } 400 | if err := r.FileCleaner(lfiles[:], oldfiles[:]); err != nil { 401 | return err 402 | } 403 | if err := r.FinalPhase(); err != nil { 404 | return err 405 | } 406 | return nil 407 | } 408 | 409 | -------------------------------------------------------------------------------- /rsync/sender.go: -------------------------------------------------------------------------------- 1 | package rsync 2 | 3 | import ( 4 | "bytes" 5 | ) 6 | 7 | type Sender struct { 8 | conn *Conn 9 | module string 10 | path string 11 | seed int32 12 | lVer int32 13 | rVer int32 14 | storage FS 15 | } 16 | 17 | func (s *Sender) SendFileList() error { 18 | list, err := s.storage.List() 19 | if err != nil { 20 | return err 21 | } 22 | 23 | // Send list to receiver 24 | var last *FileInfo = nil 25 | for _, f := range list{ 26 | var flags byte = 0 27 | 28 | 29 | if bytes.Equal(f.Path, []byte(".")) { 30 | if f.Mode.IsDIR() { 31 | flags |= FLIST_TOP_LEVEL 32 | } 33 | } else { 34 | if f.Mode.IsDIR() { // TODO: recursive 35 | // flags |= Flags.NO_CONTENT_DIR | Flags.XFLAGS; 36 | } 37 | } 38 | 39 | lPathCount := 0 40 | if last != nil { 41 | lPathCount = longestMatch(last.Path, f.Path) 42 | if lPathCount > 255 { // Limit to 255 chars 43 | lPathCount = 255 44 | } 45 | if lPathCount > 0 { 46 | flags |= FLIST_NAME_SAME 47 | } 48 | if last.Mode == f.Mode { 49 | flags |= FLIST_MODE_SAME 50 | } 51 | if last.Mtime == f.Mtime { 52 | flags |= FLIST_TIME_SAME 53 | } 54 | // 55 | // 56 | // 57 | } 58 | 59 | rPathCount := int32(len(f.Path) - lPathCount) 60 | if rPathCount > 255 { 61 | flags |= FLIST_NAME_LONG 62 | } 63 | 64 | /* we must make sure we don't send a zero flags byte or the other 65 | end will terminate the flist transfer */ 66 | if flags == 0 && !f.Mode.IsDIR() { 67 | flags |= 1<<0 68 | } 69 | if flags == 0 { 70 | flags |= FLIST_NAME_LONG 71 | } 72 | /* Send flags */ 73 | if err != s.conn.WriteByte(flags) { 74 | return err 75 | } 76 | 77 | /* Send len of path, and bytes of path */ 78 | if flags& FLIST_NAME_SAME != 0 { 79 | if err = s.conn.WriteByte(flags); err != nil { 80 | return err 81 | } 82 | } 83 | 84 | if flags& FLIST_NAME_LONG != 0 { 85 | if err = s.conn.WriteInt(rPathCount); err != nil { 86 | return err 87 | } 88 | } else { 89 | if err = s.conn.WriteByte(byte(rPathCount)); err != nil { 90 | return err 91 | } 92 | } 93 | 94 | if _, err = s.conn.Write(f.Path[lPathCount:]); err != nil { 95 | return err 96 | } 97 | 98 | /* Send size of file */ 99 | if err = s.conn.WriteLong(f.Size); err != nil { 100 | return err 101 | } 102 | 103 | /* Send Mtime, GID, UID, RDEV if needed */ 104 | if flags& FLIST_TIME_SAME == 0 { 105 | if err = s.conn.WriteInt(f.Mtime); err != nil { 106 | return err 107 | } 108 | } 109 | if flags& FLIST_MODE_SAME == 0 { 110 | if err = s.conn.WriteInt(int32(f.Mode)); err != nil { 111 | return err 112 | } 113 | } 114 | // TODO: UID GID RDEV 115 | 116 | // TODO: Send symlink 117 | 118 | // TODO: if always_checksum? 119 | 120 | last = &f 121 | } 122 | return nil 123 | } 124 | 125 | func (s *Sender) Generator(fileList FileList) error { 126 | for { 127 | index, err := s.conn.ReadInt() 128 | if err != nil { 129 | return err 130 | } else if index == INDEX_END { 131 | break 132 | } 133 | 134 | // Receive block checksum from receiver 135 | count, err := s.conn.ReadInt() 136 | if err != nil { 137 | return nil 138 | } 139 | 140 | blen, err := s.conn.ReadInt() 141 | if err != nil { 142 | return nil 143 | } 144 | 145 | s2len, err := s.conn.ReadInt() 146 | if err != nil { 147 | return nil 148 | } else if s2len > 16 { 149 | // FIXME: check if sum2 length is valid 150 | } 151 | 152 | remainder, err := s.conn.ReadInt() 153 | if err != nil { 154 | return nil 155 | } 156 | 157 | sums := make([]SumChunk, 0, count) 158 | 159 | var ( 160 | i int32 = 0 161 | offset int64 = 0 162 | ) 163 | 164 | 165 | for ; i < count; i++ { 166 | sum1, err := s.conn.ReadInt() // sum1: 167 | if err != nil { 168 | return err 169 | } 170 | 171 | sum2 := make([]byte, 16) 172 | if _, err := s.conn.Read(sum2); err != nil { 173 | return err 174 | } 175 | 176 | chunk := new(SumChunk) 177 | chunk.sum1 = uint32(sum1) 178 | chunk.sum2 = sum2 179 | chunk.fileOffset = offset 180 | 181 | if i == count-1 && remainder != 0 { 182 | chunk.chunkLen = uint(remainder); 183 | } else { 184 | chunk.chunkLen = uint(blen) 185 | } 186 | offset += int64(chunk.chunkLen) 187 | sums = append(sums, ) 188 | } 189 | result := new(SumStruct) 190 | result.fileLen = uint64(offset) 191 | result.count = uint64(count) 192 | result.blockLen = uint64(blen) 193 | result.sum2Len = uint64(s2len) 194 | result.remainder = uint64(remainder) 195 | 196 | } 197 | if err := s.FileUploader(); err != nil { 198 | return err 199 | } 200 | return nil 201 | } 202 | 203 | func (s *Sender) FileUploader() error { 204 | panic("Not implemented yet"); 205 | return nil 206 | } 207 | 208 | func (s *Sender) FinalPhase() error { 209 | panic("Not implemented yet"); 210 | return nil 211 | } 212 | 213 | func (s *Sender) Sync() error { 214 | panic("Not implemented yet"); 215 | return nil 216 | } -------------------------------------------------------------------------------- /rsync/ssh.go: -------------------------------------------------------------------------------- 1 | package rsync 2 | 3 | import ( 4 | "golang.org/x/crypto/ssh" 5 | ) 6 | 7 | type SSH struct { 8 | client *ssh.Client 9 | session *ssh.Session 10 | } 11 | 12 | func NewSSH(address string, username string, pwd string, cmd string) (*SSH, error) { 13 | config := &ssh.ClientConfig{ 14 | Config: ssh.Config{}, 15 | User: "", 16 | Auth: nil, 17 | HostKeyCallback: nil, 18 | BannerCallback: nil, 19 | ClientVersion: "", 20 | HostKeyAlgorithms: nil, 21 | Timeout: 0, 22 | } 23 | client, err := ssh.Dial("tcp", address, config) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | session, err := client.NewSession() 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | // Call remote rsync server 34 | if err := session.Run(cmd); err != nil { 35 | return nil, err 36 | } 37 | 38 | // Handshake over SSH 39 | 40 | 41 | return &SSH{ 42 | client: client, 43 | session: session, 44 | }, nil 45 | } 46 | 47 | 48 | func (s *SSH) Write(p []byte) (n int, err error) { 49 | return s.session.Stdout.Write(p) 50 | } 51 | 52 | func (s *SSH) Read(p []byte) (n int, err error) { 53 | return s.session.Stdin.Read(p) 54 | } 55 | 56 | func (s *SSH) Close() error { 57 | return s.client.Close() 58 | } 59 | 60 | -------------------------------------------------------------------------------- /rsync/utils.go: -------------------------------------------------------------------------------- 1 | package rsync 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | "log" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | func SplitURIS(uri string) (string, int, string, string, error) { 11 | 12 | var host, module, path string 13 | first := uri 14 | var second string 15 | 16 | if strings.HasPrefix(uri, "rsync://") { 17 | /* rsync://host[:port]/module[/path] */ 18 | first = first[8:] 19 | i := strings.IndexByte(first, '/') 20 | if i == -1 { 21 | // No module name 22 | panic("No module name") 23 | } 24 | second = first[i+1:] //ignore '/' 25 | first = first[:i] 26 | } else { 27 | // Only for remote 28 | /* host::module[/path] */ 29 | panic("No implement yet") 30 | } 31 | 32 | port := 873 // Default port: 873 33 | 34 | // Parse port 35 | i := strings.IndexByte(first, ':') 36 | if i != -1 { 37 | var err error 38 | port, err = strconv.Atoi(string(first[i+1:])) 39 | if err != nil { 40 | // Wrong port 41 | panic("Wrong port") 42 | } 43 | first = first[:i] 44 | } 45 | host = first 46 | 47 | // Parse path 48 | i = strings.IndexByte(second, '/') 49 | if i != -1 { 50 | path = second[i:] 51 | second = second[:i] 52 | } 53 | module = second 54 | 55 | return host, port, module, path, nil 56 | 57 | } 58 | 59 | // For rsync 60 | func SplitURI(uri string) (string, string, string, error) { 61 | 62 | var address, module, path string 63 | var first = uri 64 | var second string 65 | 66 | if strings.HasPrefix(uri, "rsync://") { 67 | /* rsync://host[:port]/module[/path] */ 68 | first = first[8:] 69 | i := strings.IndexByte(first, '/') 70 | if i == -1 { 71 | // No module name 72 | return "", "", "", errors.New("No module name") 73 | } 74 | second = first[i+1:] //ignore '/' 75 | first = first[:i] 76 | } else { 77 | // Only for remote 78 | /* host::module[/path] */ 79 | log.Fatalln("No implement yet") 80 | } 81 | 82 | address = first 83 | // Parse port 84 | i := strings.IndexByte(first, ':') 85 | if i == -1 { 86 | address += ":873" // Default port: 873 87 | } 88 | 89 | // Parse path 90 | i = strings.IndexByte(second, '/') 91 | if i != -1 { 92 | path = second[i:] 93 | second = second[:i] 94 | } 95 | module = second 96 | 97 | return address, module, path, nil 98 | 99 | } 100 | 101 | // The path always has a trailing slash appended 102 | func TrimPrepath(prepath string) string { 103 | //pre-path shouldn't use "/" as prefix, and must have a "/" suffix 104 | //pre-path can be: "xx", "xx/", "/xx", "/xx/", "", "/" 105 | ppath := prepath 106 | if !strings.HasSuffix(ppath, "/") { 107 | ppath += "/" 108 | } 109 | if strings.HasPrefix(ppath, "/") { 110 | ppath = ppath[1:] 111 | } 112 | return ppath 113 | } 114 | 115 | func longestMatch(left []byte, right []byte) int { 116 | i := 0 117 | for ; i < len(left) && i < len(right) && i < 256; i++ { 118 | if left[i] != right[i] { 119 | break 120 | } 121 | } 122 | return i; 123 | } -------------------------------------------------------------------------------- /storage/cache/finfo.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.24.0 4 | // protoc v3.12.0 5 | // source: finfo.proto 6 | 7 | package cache 8 | 9 | import ( 10 | proto "github.com/golang/protobuf/proto" 11 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 12 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 13 | reflect "reflect" 14 | sync "sync" 15 | ) 16 | 17 | const ( 18 | // Verify that this generated code is sufficiently up-to-date. 19 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 20 | // Verify that runtime/protoimpl is sufficiently up-to-date. 21 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 22 | ) 23 | 24 | // This is a compile-time assertion that a sufficiently up-to-date version 25 | // of the legacy proto package is being used. 26 | const _ = proto.ProtoPackageIsVersion4 27 | 28 | type FInfo struct { 29 | state protoimpl.MessageState 30 | sizeCache protoimpl.SizeCache 31 | unknownFields protoimpl.UnknownFields 32 | 33 | Size int64 `protobuf:"varint,1,opt,name=Size,proto3" json:"Size,omitempty"` 34 | Mtime int32 `protobuf:"varint,2,opt,name=Mtime,proto3" json:"Mtime,omitempty"` 35 | Mode int32 `protobuf:"varint,3,opt,name=Mode,proto3" json:"Mode,omitempty"` // hash 16bytes 36 | } 37 | 38 | func (x *FInfo) Reset() { 39 | *x = FInfo{} 40 | if protoimpl.UnsafeEnabled { 41 | mi := &file_finfo_proto_msgTypes[0] 42 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 43 | ms.StoreMessageInfo(mi) 44 | } 45 | } 46 | 47 | func (x *FInfo) String() string { 48 | return protoimpl.X.MessageStringOf(x) 49 | } 50 | 51 | func (*FInfo) ProtoMessage() {} 52 | 53 | func (x *FInfo) ProtoReflect() protoreflect.Message { 54 | mi := &file_finfo_proto_msgTypes[0] 55 | if protoimpl.UnsafeEnabled && x != nil { 56 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 57 | if ms.LoadMessageInfo() == nil { 58 | ms.StoreMessageInfo(mi) 59 | } 60 | return ms 61 | } 62 | return mi.MessageOf(x) 63 | } 64 | 65 | // Deprecated: Use FInfo.ProtoReflect.Descriptor instead. 66 | func (*FInfo) Descriptor() ([]byte, []int) { 67 | return file_finfo_proto_rawDescGZIP(), []int{0} 68 | } 69 | 70 | func (x *FInfo) GetSize() int64 { 71 | if x != nil { 72 | return x.Size 73 | } 74 | return 0 75 | } 76 | 77 | func (x *FInfo) GetMtime() int32 { 78 | if x != nil { 79 | return x.Mtime 80 | } 81 | return 0 82 | } 83 | 84 | func (x *FInfo) GetMode() int32 { 85 | if x != nil { 86 | return x.Mode 87 | } 88 | return 0 89 | } 90 | 91 | var File_finfo_proto protoreflect.FileDescriptor 92 | 93 | var file_finfo_proto_rawDesc = []byte{ 94 | 0x0a, 0x0b, 0x66, 0x69, 0x6e, 0x66, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x70, 95 | 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x45, 0x0a, 0x05, 0x46, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x12, 0x0a, 96 | 0x04, 0x53, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x53, 0x69, 0x7a, 97 | 0x65, 0x12, 0x14, 0x0a, 0x05, 0x4d, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 98 | 0x52, 0x05, 0x4d, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x4d, 0x6f, 0x64, 0x65, 0x18, 99 | 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x4d, 0x6f, 0x64, 0x65, 0x42, 0x21, 0x5a, 0x1f, 0x67, 100 | 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6b, 0x61, 0x69, 0x61, 0x6b, 0x7a, 101 | 0x2f, 0x72, 0x73, 0x79, 0x6e, 0x63, 0x2d, 0x6f, 0x73, 0x2f, 0x66, 0x6c, 0x64, 0x62, 0x62, 0x06, 102 | 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 103 | } 104 | 105 | var ( 106 | file_finfo_proto_rawDescOnce sync.Once 107 | file_finfo_proto_rawDescData = file_finfo_proto_rawDesc 108 | ) 109 | 110 | func file_finfo_proto_rawDescGZIP() []byte { 111 | file_finfo_proto_rawDescOnce.Do(func() { 112 | file_finfo_proto_rawDescData = protoimpl.X.CompressGZIP(file_finfo_proto_rawDescData) 113 | }) 114 | return file_finfo_proto_rawDescData 115 | } 116 | 117 | var file_finfo_proto_msgTypes = make([]protoimpl.MessageInfo, 1) 118 | var file_finfo_proto_goTypes = []interface{}{ 119 | (*FInfo)(nil), // 0: proto.FInfo 120 | } 121 | var file_finfo_proto_depIdxs = []int32{ 122 | 0, // [0:0] is the sub-list for method output_type 123 | 0, // [0:0] is the sub-list for method input_type 124 | 0, // [0:0] is the sub-list for extension type_name 125 | 0, // [0:0] is the sub-list for extension extendee 126 | 0, // [0:0] is the sub-list for field type_name 127 | } 128 | 129 | func init() { file_finfo_proto_init() } 130 | func file_finfo_proto_init() { 131 | if File_finfo_proto != nil { 132 | return 133 | } 134 | if !protoimpl.UnsafeEnabled { 135 | file_finfo_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 136 | switch v := v.(*FInfo); i { 137 | case 0: 138 | return &v.state 139 | case 1: 140 | return &v.sizeCache 141 | case 2: 142 | return &v.unknownFields 143 | default: 144 | return nil 145 | } 146 | } 147 | } 148 | type x struct{} 149 | out := protoimpl.TypeBuilder{ 150 | File: protoimpl.DescBuilder{ 151 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 152 | RawDescriptor: file_finfo_proto_rawDesc, 153 | NumEnums: 0, 154 | NumMessages: 1, 155 | NumExtensions: 0, 156 | NumServices: 0, 157 | }, 158 | GoTypes: file_finfo_proto_goTypes, 159 | DependencyIndexes: file_finfo_proto_depIdxs, 160 | MessageInfos: file_finfo_proto_msgTypes, 161 | }.Build() 162 | File_finfo_proto = out.File 163 | file_finfo_proto_rawDesc = nil 164 | file_finfo_proto_goTypes = nil 165 | file_finfo_proto_depIdxs = nil 166 | } 167 | -------------------------------------------------------------------------------- /storage/cache/finfo.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package proto; 3 | 4 | option go_package = "github.com/kaiakz/rsync-os/fldb"; 5 | 6 | message FInfo { 7 | int64 Size = 1; 8 | int32 Mtime = 2; 9 | int32 Mode = 3; 10 | // hash 16bytes 11 | } -------------------------------------------------------------------------------- /storage/cache/pbgen.sh: -------------------------------------------------------------------------------- 1 | export PATH=$PATH:~/go/bin # Where protoc-gen-go is 2 | protoc -I=. --go_out=. --go_opt=paths=source_relative finfo.proto 3 | # protoc -I=fldb/ --go_out=fldb/ --go_opt=paths=source_relative fldb/finfo.proto -------------------------------------------------------------------------------- /storage/cache/redis.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | -------------------------------------------------------------------------------- /storage/cache/utils.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "github.com/golang/protobuf/proto" 5 | bolt "go.etcd.io/bbolt" 6 | "log" 7 | "rsync-os/rsync" 8 | "time" 9 | ) 10 | 11 | // Test 12 | func Snapshot(list rsync.FileList, module string, prepath string) { 13 | startTime := time.Now() 14 | db, err := bolt.Open("test.db", 0666, nil) 15 | defer db.Close() 16 | if err != nil { 17 | return 18 | } 19 | 20 | err = db.Update(func(tx *bolt.Tx) error { 21 | bucket := tx.Bucket([]byte(module)) 22 | // If bucket does not exist, create the bucket 23 | if bucket == nil { 24 | var err error 25 | bucket, err = tx.CreateBucket([]byte(module)) 26 | if err != nil { 27 | return err 28 | } 29 | } 30 | for _, info := range list { 31 | key := append([]byte(prepath), info.Path[:]...) 32 | value, err := proto.Marshal(&FInfo{ 33 | Size: info.Size, 34 | Mtime: info.Mtime, 35 | Mode: int32(info.Mode), // FIXME: convert uint32 to int32 36 | }) 37 | if err != nil { 38 | log.Println("Marshal failed", err) 39 | return err 40 | } 41 | err = bucket.Put(key, value) 42 | if err != nil { 43 | return err 44 | } 45 | } 46 | return nil 47 | }) 48 | if err != nil { 49 | log.Println("Update failed", err) 50 | } 51 | log.Println("Saved Duration", time.Since(startTime)) 52 | } -------------------------------------------------------------------------------- /storage/local.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | "rsync-os/rsync" 10 | ) 11 | 12 | type Local struct { 13 | workDir string // Module + Path 14 | } 15 | 16 | func NewLocal(module string, path string, topDir string) (*Local, error) { 17 | // First, creates a module folder under topDir 18 | workDir := filepath.Join(topDir, module, path) 19 | if err := os.MkdirAll(workDir, os.ModePerm); err != nil { 20 | return nil, err 21 | } 22 | return &Local{workDir: workDir}, nil 23 | } 24 | 25 | func (l *Local) Put(fileName string, content io.Reader, fileSize int64, metadata rsync.FileMetadata) (written int64, err error) { 26 | fpath := filepath.Join(l.workDir, fileName) 27 | // if the file is a folder, ignores content, just creates a folder under the workDir 28 | if metadata.Mode.IsDIR() { 29 | return 0, os.Mkdir(fpath, os.ModePerm) 30 | } 31 | 32 | if metadata.Mode.IsREG() { 33 | f, err := os.OpenFile(fpath, os.O_CREATE | os.O_EXCL | os.O_WRONLY, metadata.Mode.Convert()) 34 | if err != nil { 35 | return -1, err 36 | } 37 | defer f.Close() 38 | 39 | // Craete a buffer 40 | fb := bufio.NewWriter(f) 41 | defer fb.Flush() 42 | 43 | return io.Copy(fb, content) 44 | } 45 | 46 | return -2, errors.New("Do not support type " + fileName + metadata.Mode.String()) 47 | } 48 | 49 | func (l *Local) Delete(fileName string, mode rsync.FileMode) error { 50 | if mode.IsDIR() { 51 | return os.RemoveAll(fileName) 52 | } 53 | return os.Remove(fileName) 54 | } 55 | 56 | func (l *Local) List() (rsync.FileList, error) { 57 | filelist := make(rsync.FileList, 0, 1 << 16) 58 | 59 | if err := filepath.Walk(l.workDir, func(path string, info os.FileInfo, err error) error { 60 | if err != nil { 61 | return err 62 | } 63 | 64 | filelist = append(filelist, rsync.FileInfo{ 65 | Path: []byte(info.Name()), 66 | Size: info.Size(), 67 | Mtime: int32(info.ModTime().Unix()), // FIXME 68 | Mode: rsync.NewFileMode(info.Mode()), 69 | }) 70 | 71 | return nil 72 | }); err != nil { 73 | return filelist, err 74 | } 75 | return filelist, nil 76 | } 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /storage/minio.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "bytes" 5 | "github.com/golang/protobuf/proto" 6 | "github.com/minio/minio-go/v6" 7 | bolt "go.etcd.io/bbolt" 8 | "io" 9 | "log" 10 | "path" 11 | "path/filepath" 12 | "rsync-os/rsync" 13 | "rsync-os/storage/cache" 14 | "sort" 15 | "strconv" 16 | "strings" 17 | ) 18 | 19 | /* 20 | rsync-os will add addition information for each file that was uploaded to minio 21 | rsync-os stores the information of a folder in the metadata of an empty file called "..." 22 | rsync-os also uses a strange file to represent a soft link 23 | */ 24 | 25 | // S3 with cache 26 | type Minio struct { 27 | client *minio.Client 28 | bucketName string 29 | prefix string 30 | /* Cache */ 31 | cache *bolt.DB 32 | tx *bolt.Tx 33 | bucket *bolt.Bucket 34 | } 35 | 36 | const S3_DIR = ".dir.rsync-os" 37 | 38 | func NewMinio(bucket string, prefix string, cachePath string, endpoint string, accessKeyID string, secretAccessKey string, secure bool) (*Minio, error) { 39 | minioClient, err := minio.New(endpoint, accessKeyID, secretAccessKey, false) 40 | if err != nil { 41 | panic("Failed to init a minio client") 42 | } 43 | // Create a bucket for the module 44 | err = minioClient.MakeBucket(bucket, "us-east-1") 45 | if err != nil { 46 | // Check to see if we already own this bucket (which happens if you run this twice) 47 | exists, errBucketExists := minioClient.BucketExists(bucket) 48 | if errBucketExists == nil && exists { 49 | log.Printf("We already own %s\n", bucket) 50 | } else { 51 | log.Fatalln(err) 52 | } 53 | } else { 54 | log.Printf("Successfully created %s\n", bucket) 55 | } 56 | 57 | // Initialize cache 58 | db, err := bolt.Open(cachePath, 0666, nil) 59 | if err != nil { 60 | panic("Can't init cache: boltdb") 61 | } 62 | tx, err := db.Begin(true) 63 | if err != nil { 64 | panic(err) 65 | } 66 | 67 | // If bucket does not exist, create the bucket 68 | mod, err := tx.CreateBucketIfNotExists([]byte(bucket)) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | return &Minio{ 74 | client: minioClient, 75 | bucketName: bucket, 76 | prefix: prefix, 77 | cache: db, 78 | tx: tx, 79 | bucket: mod, 80 | }, nil 81 | } 82 | 83 | // object can be a regualar file, folder or symlink 84 | func (m *Minio) Put(fileName string, content io.Reader, fileSize int64, metadata rsync.FileMetadata) (written int64, err error) { 85 | data := make(map[string]string) 86 | data["mtime"] = strconv.Itoa(int(metadata.Mtime)) 87 | data["mode"] = strconv.Itoa(int(metadata.Mode)) 88 | 89 | fpath := filepath.Join(m.prefix, fileName) 90 | fsize := fileSize 91 | fname := fpath 92 | /* EXPERIMENTAL */ 93 | // Folder 94 | if metadata.Mode.IsDIR() { 95 | fname = filepath.Join(m.prefix, fileName, S3_DIR) 96 | fsize = 0 97 | // FIXME: How to handle a file named ".rsync-os.dir"? 98 | } 99 | 100 | if metadata.Mode.IsLNK() { 101 | // Additional data of symbol link 102 | } 103 | 104 | written, err = m.client.PutObject(m.bucketName, fname, content, fsize, minio.PutObjectOptions{UserMetadata: data}) 105 | 106 | value, err := proto.Marshal(&cache.FInfo{ 107 | Size: fileSize, 108 | Mtime: metadata.Mtime, 109 | Mode: int32(metadata.Mode), // FIXME: convert uint32 to int32 110 | }) 111 | if err != nil { 112 | return -1, err 113 | } 114 | if err := m.bucket.Put([]byte(fpath), value); err != nil { 115 | return -1, err 116 | } 117 | 118 | return 119 | } 120 | 121 | func (m *Minio) Delete(fileName string, mode rsync.FileMode) (err error) { 122 | fpath := filepath.Join(m.prefix, fileName) 123 | // TODO: How to delete a folder 124 | if mode.IsDIR() { 125 | err = m.client.RemoveObject(m.bucketName, filepath.Join(fpath, S3_DIR)) 126 | } else { 127 | if err = m.client.RemoveObject(m.bucketName, fpath); err != nil { 128 | return 129 | } 130 | } 131 | log.Println(fileName) 132 | return m.bucket.Delete([]byte(fpath)) 133 | } 134 | 135 | // EXPERIMENTAL 136 | func (m *Minio) List() (rsync.FileList, error) { 137 | filelist := make(rsync.FileList, 0, 1 << 16) 138 | 139 | // We don't list all files directly 140 | 141 | info := &cache.FInfo{} 142 | 143 | // Add files in the work dir 144 | c := m.bucket.Cursor() 145 | prefix := []byte(m.prefix) 146 | k, v := c.Seek(prefix) 147 | hasdot := false 148 | for k != nil && bytes.HasPrefix(k, prefix) { 149 | p := k[len(prefix):] 150 | if bytes.Equal(p, []byte(".")) { 151 | hasdot = true 152 | } 153 | 154 | if err := proto.Unmarshal(v, info); err != nil { 155 | return filelist, err 156 | } 157 | filelist = append(filelist, rsync.FileInfo{ 158 | Path: p, // ignore prefix 159 | Size: info.Size, 160 | Mtime: info.Mtime, 161 | Mode: rsync.FileMode(info.Mode), 162 | }) 163 | k, v = c.Next() 164 | } 165 | 166 | // Add current dir as . 167 | if !hasdot { 168 | workdir := []byte(filepath.Clean(m.prefix)) // If a empty string, we get "." 169 | v := m.bucket.Get(workdir) 170 | if v == nil { 171 | return filelist, nil 172 | } 173 | if err := proto.Unmarshal(v, info); err != nil { 174 | return filelist, err 175 | } 176 | filelist = append(filelist, rsync.FileInfo{ 177 | Path: []byte("."), 178 | Size: info.Size, 179 | Mtime: info.Mtime, 180 | Mode: rsync.FileMode(info.Mode), 181 | }) 182 | } 183 | 184 | sort.Sort(filelist) 185 | 186 | return filelist, nil 187 | } 188 | 189 | func (m *Minio) ListObj() (rsync.FileList, error) { 190 | filelist := make(rsync.FileList, 0, 1 << 16) 191 | 192 | // Create a done channel to control 'ListObjects' go routine. 193 | doneCh := make(chan struct{}) 194 | 195 | // Indicate to our routine to exit cleanly upon return. 196 | defer close(doneCh) 197 | 198 | // FIXME: objectPrefix, recursive 199 | objectCh := m.client.ListObjectsV2(m.bucketName, "", true, doneCh) 200 | for object := range objectCh { 201 | if object.Err != nil { 202 | log.Println(object.Err) 203 | return filelist, object.Err 204 | } 205 | 206 | // FIXME: Handle folder 207 | objectName := object.Key 208 | if strings.Compare(path.Base(objectName), "...") == 0 { 209 | objectName = path.Dir(objectName) 210 | } 211 | 212 | mtime, err := strconv.Atoi(object.UserMetadata["mtime"]) 213 | if err != nil { 214 | panic("Can't get the mode from minio") 215 | } 216 | 217 | mode, err := strconv.Atoi(object.UserMetadata["mtime"]) 218 | if err != nil { 219 | panic("Can't get the mode from minio") 220 | } 221 | 222 | filelist = append(filelist, rsync.FileInfo{ 223 | Path: []byte(objectName), 224 | Size: object.Size, 225 | Mtime: int32(mtime), 226 | Mode: rsync.FileMode(mode), 227 | }) 228 | } 229 | return filelist, nil 230 | } 231 | 232 | func (m *Minio) Close() error { 233 | defer m.cache.Close() 234 | if err := m.tx.Commit(); err != nil { 235 | return err 236 | } 237 | return nil 238 | } 239 | --------------------------------------------------------------------------------