You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

352 lines
10 KiB

7 years ago
7 years ago
7 years ago
  1. package command
  2. import (
  3. "fmt"
  4. "github.com/chrislusf/seaweedfs/weed/security"
  5. "github.com/chrislusf/seaweedfs/weed/server"
  6. "github.com/chrislusf/seaweedfs/weed/wdclient"
  7. "github.com/spf13/viper"
  8. "google.golang.org/grpc"
  9. "io/ioutil"
  10. "net/url"
  11. "os"
  12. "path/filepath"
  13. "strings"
  14. "context"
  15. "github.com/chrislusf/seaweedfs/weed/operation"
  16. "github.com/chrislusf/seaweedfs/weed/pb/filer_pb"
  17. "github.com/chrislusf/seaweedfs/weed/util"
  18. "io"
  19. "net/http"
  20. "strconv"
  21. "time"
  22. )
  23. var (
  24. copy CopyOptions
  25. )
  26. type CopyOptions struct {
  27. filerGrpcPort *int
  28. master *string
  29. include *string
  30. replication *string
  31. collection *string
  32. ttl *string
  33. maxMB *int
  34. grpcDialOption grpc.DialOption
  35. masterClient *wdclient.MasterClient
  36. }
  37. func init() {
  38. cmdCopy.Run = runCopy // break init cycle
  39. cmdCopy.IsDebug = cmdCopy.Flag.Bool("debug", false, "verbose debug information")
  40. copy.master = cmdCopy.Flag.String("master", "localhost:9333", "SeaweedFS master location")
  41. copy.include = cmdCopy.Flag.String("include", "", "pattens of files to copy, e.g., *.pdf, *.html, ab?d.txt, works together with -dir")
  42. copy.replication = cmdCopy.Flag.String("replication", "", "replication type")
  43. copy.collection = cmdCopy.Flag.String("collection", "", "optional collection name")
  44. copy.ttl = cmdCopy.Flag.String("ttl", "", "time to live, e.g.: 1m, 1h, 1d, 1M, 1y")
  45. copy.maxMB = cmdCopy.Flag.Int("maxMB", 0, "split files larger than the limit")
  46. copy.filerGrpcPort = cmdCopy.Flag.Int("filer.port.grpc", 0, "filer grpc server listen port, default to filer port + 10000")
  47. }
  48. var cmdCopy = &Command{
  49. UsageLine: "filer.copy file_or_dir1 [file_or_dir2 file_or_dir3] http://localhost:8888/path/to/a/folder/",
  50. Short: "copy one or a list of files to a filer folder",
  51. Long: `copy one or a list of files, or batch copy one whole folder recursively, to a filer folder
  52. It can copy one or a list of files or folders.
  53. If copying a whole folder recursively:
  54. All files under the folder and subfolders will be copyed.
  55. Optional parameter "-include" allows you to specify the file name patterns.
  56. If "maxMB" is set to a positive number, files larger than it would be split into chunks.
  57. `,
  58. }
  59. func runCopy(cmd *Command, args []string) bool {
  60. weed_server.LoadConfiguration("security", false)
  61. if len(args) <= 1 {
  62. return false
  63. }
  64. filerDestination := args[len(args)-1]
  65. fileOrDirs := args[0 : len(args)-1]
  66. filerUrl, err := url.Parse(filerDestination)
  67. if err != nil {
  68. fmt.Printf("The last argument should be a URL on filer: %v\n", err)
  69. return false
  70. }
  71. urlPath := filerUrl.Path
  72. if !strings.HasSuffix(urlPath, "/") {
  73. fmt.Printf("The last argument should be a folder and end with \"/\": %v\n", err)
  74. return false
  75. }
  76. if filerUrl.Port() == "" {
  77. fmt.Printf("The filer port should be specified.\n")
  78. return false
  79. }
  80. filerPort, parseErr := strconv.ParseUint(filerUrl.Port(), 10, 64)
  81. if parseErr != nil {
  82. fmt.Printf("The filer port parse error: %v\n", parseErr)
  83. return false
  84. }
  85. filerGrpcPort := filerPort + 10000
  86. if *copy.filerGrpcPort != 0 {
  87. filerGrpcPort = uint64(*copy.filerGrpcPort)
  88. }
  89. filerGrpcAddress := fmt.Sprintf("%s:%d", filerUrl.Hostname(), filerGrpcPort)
  90. copy.grpcDialOption = security.LoadClientTLS(viper.Sub("grpc"), "client")
  91. copy.masterClient = wdclient.NewMasterClient(context.Background(), copy.grpcDialOption, "client", strings.Split(*copy.master, ","))
  92. go copy.masterClient.KeepConnectedToMaster()
  93. copy.masterClient.WaitUntilConnected()
  94. for _, fileOrDir := range fileOrDirs {
  95. if !doEachCopy(context.Background(), fileOrDir, filerUrl.Host, filerGrpcAddress, copy.grpcDialOption, urlPath) {
  96. return false
  97. }
  98. }
  99. return true
  100. }
  101. func doEachCopy(ctx context.Context, fileOrDir string, filerAddress, filerGrpcAddress string, grpcDialOption grpc.DialOption, path string) bool {
  102. f, err := os.Open(fileOrDir)
  103. if err != nil {
  104. fmt.Printf("Failed to open file %s: %v\n", fileOrDir, err)
  105. return false
  106. }
  107. defer f.Close()
  108. fi, err := f.Stat()
  109. if err != nil {
  110. fmt.Printf("Failed to get stat for file %s: %v\n", fileOrDir, err)
  111. return false
  112. }
  113. mode := fi.Mode()
  114. if mode.IsDir() {
  115. files, _ := ioutil.ReadDir(fileOrDir)
  116. for _, subFileOrDir := range files {
  117. if !doEachCopy(ctx, fileOrDir+"/"+subFileOrDir.Name(), filerAddress, filerGrpcAddress, grpcDialOption, path+fi.Name()+"/") {
  118. return false
  119. }
  120. }
  121. return true
  122. }
  123. // this is a regular file
  124. if *copy.include != "" {
  125. if ok, _ := filepath.Match(*copy.include, filepath.Base(fileOrDir)); !ok {
  126. return true
  127. }
  128. }
  129. // find the chunk count
  130. chunkSize := int64(*copy.maxMB * 1024 * 1024)
  131. chunkCount := 1
  132. if chunkSize > 0 && fi.Size() > chunkSize {
  133. chunkCount = int(fi.Size()/chunkSize) + 1
  134. }
  135. if chunkCount == 1 {
  136. return uploadFileAsOne(ctx, filerAddress, filerGrpcAddress, grpcDialOption, path, f, fi)
  137. }
  138. return uploadFileInChunks(ctx, filerAddress, filerGrpcAddress, grpcDialOption, path, f, fi, chunkCount, chunkSize)
  139. }
  140. func uploadFileAsOne(ctx context.Context, filerAddress, filerGrpcAddress string, grpcDialOption grpc.DialOption, urlFolder string, f *os.File, fi os.FileInfo) bool {
  141. // upload the file content
  142. fileName := filepath.Base(f.Name())
  143. mimeType := detectMimeType(f)
  144. var chunks []*filer_pb.FileChunk
  145. if fi.Size() > 0 {
  146. // assign a volume
  147. assignResult, err := operation.Assign(copy.masterClient.GetMaster(), grpcDialOption, &operation.VolumeAssignRequest{
  148. Count: 1,
  149. Replication: *copy.replication,
  150. Collection: *copy.collection,
  151. Ttl: *copy.ttl,
  152. })
  153. if err != nil {
  154. fmt.Printf("Failed to assign from %s: %v\n", *copy.master, err)
  155. }
  156. targetUrl := "http://" + assignResult.Url + "/" + assignResult.Fid
  157. uploadResult, err := operation.Upload(targetUrl, fileName, f, false, mimeType, nil, assignResult.Auth)
  158. if err != nil {
  159. fmt.Printf("upload data %v to %s: %v\n", fileName, targetUrl, err)
  160. return false
  161. }
  162. if uploadResult.Error != "" {
  163. fmt.Printf("upload %v to %s result: %v\n", fileName, targetUrl, uploadResult.Error)
  164. return false
  165. }
  166. fmt.Printf("uploaded %s to %s\n", fileName, targetUrl)
  167. chunks = append(chunks, &filer_pb.FileChunk{
  168. FileId: assignResult.Fid,
  169. Offset: 0,
  170. Size: uint64(uploadResult.Size),
  171. Mtime: time.Now().UnixNano(),
  172. ETag: uploadResult.ETag,
  173. })
  174. fmt.Printf("copied %s => http://%s%s%s\n", fileName, filerAddress, urlFolder, fileName)
  175. }
  176. if err := withFilerClient(ctx, filerGrpcAddress, grpcDialOption, func(client filer_pb.SeaweedFilerClient) error {
  177. request := &filer_pb.CreateEntryRequest{
  178. Directory: urlFolder,
  179. Entry: &filer_pb.Entry{
  180. Name: fileName,
  181. Attributes: &filer_pb.FuseAttributes{
  182. Crtime: time.Now().Unix(),
  183. Mtime: time.Now().Unix(),
  184. Gid: uint32(os.Getgid()),
  185. Uid: uint32(os.Getuid()),
  186. FileSize: uint64(fi.Size()),
  187. FileMode: uint32(fi.Mode()),
  188. Mime: mimeType,
  189. Replication: *copy.replication,
  190. Collection: *copy.collection,
  191. TtlSec: int32(util.ParseInt(*copy.ttl, 0)),
  192. },
  193. Chunks: chunks,
  194. },
  195. }
  196. if _, err := client.CreateEntry(ctx, request); err != nil {
  197. return fmt.Errorf("update fh: %v", err)
  198. }
  199. return nil
  200. }); err != nil {
  201. fmt.Printf("upload data %v to http://%s%s%s: %v\n", fileName, filerAddress, urlFolder, fileName, err)
  202. return false
  203. }
  204. return true
  205. }
  206. func uploadFileInChunks(ctx context.Context, filerAddress, filerGrpcAddress string, grpcDialOption grpc.DialOption, urlFolder string, f *os.File, fi os.FileInfo, chunkCount int, chunkSize int64) bool {
  207. fileName := filepath.Base(f.Name())
  208. mimeType := detectMimeType(f)
  209. var chunks []*filer_pb.FileChunk
  210. for i := int64(0); i < int64(chunkCount); i++ {
  211. // assign a volume
  212. assignResult, err := operation.Assign(copy.masterClient.GetMaster(), grpcDialOption, &operation.VolumeAssignRequest{
  213. Count: 1,
  214. Replication: *copy.replication,
  215. Collection: *copy.collection,
  216. Ttl: *copy.ttl,
  217. })
  218. if err != nil {
  219. fmt.Printf("Failed to assign from %s: %v\n", *copy.master, err)
  220. }
  221. targetUrl := "http://" + assignResult.Url + "/" + assignResult.Fid
  222. uploadResult, err := operation.Upload(targetUrl,
  223. fileName+"-"+strconv.FormatInt(i+1, 10),
  224. io.LimitReader(f, chunkSize),
  225. false, "application/octet-stream", nil, assignResult.Auth)
  226. if err != nil {
  227. fmt.Printf("upload data %v to %s: %v\n", fileName, targetUrl, err)
  228. return false
  229. }
  230. if uploadResult.Error != "" {
  231. fmt.Printf("upload %v to %s result: %v\n", fileName, targetUrl, uploadResult.Error)
  232. return false
  233. }
  234. chunks = append(chunks, &filer_pb.FileChunk{
  235. FileId: assignResult.Fid,
  236. Offset: i * chunkSize,
  237. Size: uint64(uploadResult.Size),
  238. Mtime: time.Now().UnixNano(),
  239. ETag: uploadResult.ETag,
  240. })
  241. fmt.Printf("uploaded %s-%d to %s [%d,%d)\n", fileName, i+1, targetUrl, i*chunkSize, i*chunkSize+int64(uploadResult.Size))
  242. }
  243. if err := withFilerClient(ctx, filerGrpcAddress, grpcDialOption, func(client filer_pb.SeaweedFilerClient) error {
  244. request := &filer_pb.CreateEntryRequest{
  245. Directory: urlFolder,
  246. Entry: &filer_pb.Entry{
  247. Name: fileName,
  248. Attributes: &filer_pb.FuseAttributes{
  249. Crtime: time.Now().Unix(),
  250. Mtime: time.Now().Unix(),
  251. Gid: uint32(os.Getgid()),
  252. Uid: uint32(os.Getuid()),
  253. FileSize: uint64(fi.Size()),
  254. FileMode: uint32(fi.Mode()),
  255. Mime: mimeType,
  256. Replication: *copy.replication,
  257. Collection: *copy.collection,
  258. TtlSec: int32(util.ParseInt(*copy.ttl, 0)),
  259. },
  260. Chunks: chunks,
  261. },
  262. }
  263. if _, err := client.CreateEntry(ctx, request); err != nil {
  264. return fmt.Errorf("update fh: %v", err)
  265. }
  266. return nil
  267. }); err != nil {
  268. fmt.Printf("upload data %v to http://%s%s%s: %v\n", fileName, filerAddress, urlFolder, fileName, err)
  269. return false
  270. }
  271. fmt.Printf("copied %s => http://%s%s%s\n", fileName, filerAddress, urlFolder, fileName)
  272. return true
  273. }
  274. func detectMimeType(f *os.File) string {
  275. head := make([]byte, 512)
  276. f.Seek(0, io.SeekStart)
  277. n, err := f.Read(head)
  278. if err == io.EOF {
  279. return ""
  280. }
  281. if err != nil {
  282. fmt.Printf("read head of %v: %v\n", f.Name(), err)
  283. return "application/octet-stream"
  284. }
  285. f.Seek(0, io.SeekStart)
  286. mimeType := http.DetectContentType(head[:n])
  287. return mimeType
  288. }
  289. func withFilerClient(ctx context.Context, filerAddress string, grpcDialOption grpc.DialOption, fn func(filer_pb.SeaweedFilerClient) error) error {
  290. grpcConnection, err := util.GrpcDial(ctx, filerAddress, grpcDialOption)
  291. if err != nil {
  292. return fmt.Errorf("fail to dial %s: %v", filerAddress, err)
  293. }
  294. defer grpcConnection.Close()
  295. client := filer_pb.NewSeaweedFilerClient(grpcConnection)
  296. return fn(client)
  297. }