filer_copy.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504
  1. package command
  2. import (
  3. "context"
  4. "fmt"
  5. "io"
  6. "io/ioutil"
  7. "net/http"
  8. "net/url"
  9. "os"
  10. "path/filepath"
  11. "strconv"
  12. "strings"
  13. "sync"
  14. "time"
  15. "google.golang.org/grpc"
  16. "github.com/chrislusf/seaweedfs/weed/util/grace"
  17. "github.com/chrislusf/seaweedfs/weed/operation"
  18. "github.com/chrislusf/seaweedfs/weed/pb"
  19. "github.com/chrislusf/seaweedfs/weed/pb/filer_pb"
  20. "github.com/chrislusf/seaweedfs/weed/security"
  21. "github.com/chrislusf/seaweedfs/weed/storage/needle"
  22. "github.com/chrislusf/seaweedfs/weed/util"
  23. "github.com/chrislusf/seaweedfs/weed/wdclient"
  24. )
  25. var (
  26. copy CopyOptions
  27. waitGroup sync.WaitGroup
  28. )
  29. type CopyOptions struct {
  30. include *string
  31. replication *string
  32. collection *string
  33. ttl *string
  34. maxMB *int
  35. masterClient *wdclient.MasterClient
  36. concurrenctFiles *int
  37. concurrenctChunks *int
  38. grpcDialOption grpc.DialOption
  39. masters []string
  40. cipher bool
  41. ttlSec int32
  42. }
  43. func init() {
  44. cmdCopy.Run = runCopy // break init cycle
  45. cmdCopy.IsDebug = cmdCopy.Flag.Bool("debug", false, "verbose debug information")
  46. copy.include = cmdCopy.Flag.String("include", "", "pattens of files to copy, e.g., *.pdf, *.html, ab?d.txt, works together with -dir")
  47. copy.replication = cmdCopy.Flag.String("replication", "", "replication type")
  48. copy.collection = cmdCopy.Flag.String("collection", "", "optional collection name")
  49. copy.ttl = cmdCopy.Flag.String("ttl", "", "time to live, e.g.: 1m, 1h, 1d, 1M, 1y")
  50. copy.maxMB = cmdCopy.Flag.Int("maxMB", 32, "split files larger than the limit")
  51. copy.concurrenctFiles = cmdCopy.Flag.Int("c", 8, "concurrent file copy goroutines")
  52. copy.concurrenctChunks = cmdCopy.Flag.Int("concurrentChunks", 8, "concurrent chunk copy goroutines for each file")
  53. }
  54. var cmdCopy = &Command{
  55. UsageLine: "filer.copy file_or_dir1 [file_or_dir2 file_or_dir3] http://localhost:8888/path/to/a/folder/",
  56. Short: "copy one or a list of files to a filer folder",
  57. Long: `copy one or a list of files, or batch copy one whole folder recursively, to a filer folder
  58. It can copy one or a list of files or folders.
  59. If copying a whole folder recursively:
  60. All files under the folder and subfolders will be copyed.
  61. Optional parameter "-include" allows you to specify the file name patterns.
  62. If "maxMB" is set to a positive number, files larger than it would be split into chunks.
  63. `,
  64. }
  65. func runCopy(cmd *Command, args []string) bool {
  66. util.LoadConfiguration("security", false)
  67. if len(args) <= 1 {
  68. return false
  69. }
  70. filerDestination := args[len(args)-1]
  71. fileOrDirs := args[0 : len(args)-1]
  72. filerUrl, err := url.Parse(filerDestination)
  73. if err != nil {
  74. fmt.Printf("The last argument should be a URL on filer: %v\n", err)
  75. return false
  76. }
  77. urlPath := filerUrl.Path
  78. if !strings.HasSuffix(urlPath, "/") {
  79. fmt.Printf("The last argument should be a folder and end with \"/\": %v\n", err)
  80. return false
  81. }
  82. if filerUrl.Port() == "" {
  83. fmt.Printf("The filer port should be specified.\n")
  84. return false
  85. }
  86. filerPort, parseErr := strconv.ParseUint(filerUrl.Port(), 10, 64)
  87. if parseErr != nil {
  88. fmt.Printf("The filer port parse error: %v\n", parseErr)
  89. return false
  90. }
  91. filerGrpcPort := filerPort + 10000
  92. filerGrpcAddress := fmt.Sprintf("%s:%d", filerUrl.Hostname(), filerGrpcPort)
  93. copy.grpcDialOption = security.LoadClientTLS(util.GetViper(), "grpc.client")
  94. masters, collection, replication, maxMB, cipher, err := readFilerConfiguration(copy.grpcDialOption, filerGrpcAddress)
  95. if err != nil {
  96. fmt.Printf("read from filer %s: %v\n", filerGrpcAddress, err)
  97. return false
  98. }
  99. if *copy.collection == "" {
  100. *copy.collection = collection
  101. }
  102. if *copy.replication == "" {
  103. *copy.replication = replication
  104. }
  105. if *copy.maxMB == 0 {
  106. *copy.maxMB = int(maxMB)
  107. }
  108. copy.masters = masters
  109. copy.cipher = cipher
  110. ttl, err := needle.ReadTTL(*copy.ttl)
  111. if err != nil {
  112. fmt.Printf("parsing ttl %s: %v\n", *copy.ttl, err)
  113. return false
  114. }
  115. copy.ttlSec = int32(ttl.Minutes()) * 60
  116. if *cmdCopy.IsDebug {
  117. grace.SetupProfiling("filer.copy.cpu.pprof", "filer.copy.mem.pprof")
  118. }
  119. fileCopyTaskChan := make(chan FileCopyTask, *copy.concurrenctFiles)
  120. go func() {
  121. defer close(fileCopyTaskChan)
  122. for _, fileOrDir := range fileOrDirs {
  123. if err := genFileCopyTask(fileOrDir, urlPath, fileCopyTaskChan); err != nil {
  124. fmt.Fprintf(os.Stderr, "gen file list error: %v\n", err)
  125. break
  126. }
  127. }
  128. }()
  129. for i := 0; i < *copy.concurrenctFiles; i++ {
  130. waitGroup.Add(1)
  131. go func() {
  132. defer waitGroup.Done()
  133. worker := FileCopyWorker{
  134. options: &copy,
  135. filerHost: filerUrl.Host,
  136. filerGrpcAddress: filerGrpcAddress,
  137. }
  138. if err := worker.copyFiles(fileCopyTaskChan); err != nil {
  139. fmt.Fprintf(os.Stderr, "copy file error: %v\n", err)
  140. return
  141. }
  142. }()
  143. }
  144. waitGroup.Wait()
  145. return true
  146. }
  147. func readFilerConfiguration(grpcDialOption grpc.DialOption, filerGrpcAddress string) (masters []string, collection, replication string, maxMB uint32, cipher bool, err error) {
  148. err = pb.WithGrpcFilerClient(filerGrpcAddress, grpcDialOption, func(client filer_pb.SeaweedFilerClient) error {
  149. resp, err := client.GetFilerConfiguration(context.Background(), &filer_pb.GetFilerConfigurationRequest{})
  150. if err != nil {
  151. return fmt.Errorf("get filer %s configuration: %v", filerGrpcAddress, err)
  152. }
  153. masters, collection, replication, maxMB = resp.Masters, resp.Collection, resp.Replication, resp.MaxMb
  154. cipher = resp.Cipher
  155. return nil
  156. })
  157. return
  158. }
  159. func genFileCopyTask(fileOrDir string, destPath string, fileCopyTaskChan chan FileCopyTask) error {
  160. fi, err := os.Stat(fileOrDir)
  161. if err != nil {
  162. fmt.Fprintf(os.Stderr, "Failed to get stat for file %s: %v\n", fileOrDir, err)
  163. return nil
  164. }
  165. mode := fi.Mode()
  166. if mode.IsDir() {
  167. files, _ := ioutil.ReadDir(fileOrDir)
  168. for _, subFileOrDir := range files {
  169. if err = genFileCopyTask(fileOrDir+"/"+subFileOrDir.Name(), destPath+fi.Name()+"/", fileCopyTaskChan); err != nil {
  170. return err
  171. }
  172. }
  173. return nil
  174. }
  175. uid, gid := util.GetFileUidGid(fi)
  176. fileCopyTaskChan <- FileCopyTask{
  177. sourceLocation: fileOrDir,
  178. destinationUrlPath: destPath,
  179. fileSize: fi.Size(),
  180. fileMode: fi.Mode(),
  181. uid: uid,
  182. gid: gid,
  183. }
  184. return nil
  185. }
  186. type FileCopyWorker struct {
  187. options *CopyOptions
  188. filerHost string
  189. filerGrpcAddress string
  190. }
  191. func (worker *FileCopyWorker) copyFiles(fileCopyTaskChan chan FileCopyTask) error {
  192. for task := range fileCopyTaskChan {
  193. if err := worker.doEachCopy(task); err != nil {
  194. return err
  195. }
  196. }
  197. return nil
  198. }
  199. type FileCopyTask struct {
  200. sourceLocation string
  201. destinationUrlPath string
  202. fileSize int64
  203. fileMode os.FileMode
  204. uid uint32
  205. gid uint32
  206. }
  207. func (worker *FileCopyWorker) doEachCopy(task FileCopyTask) error {
  208. f, err := os.Open(task.sourceLocation)
  209. if err != nil {
  210. fmt.Printf("Failed to open file %s: %v\n", task.sourceLocation, err)
  211. if _, ok := err.(*os.PathError); ok {
  212. fmt.Printf("skipping %s\n", task.sourceLocation)
  213. return nil
  214. }
  215. return err
  216. }
  217. defer f.Close()
  218. // this is a regular file
  219. if *worker.options.include != "" {
  220. if ok, _ := filepath.Match(*worker.options.include, filepath.Base(task.sourceLocation)); !ok {
  221. return nil
  222. }
  223. }
  224. // find the chunk count
  225. chunkSize := int64(*worker.options.maxMB * 1024 * 1024)
  226. chunkCount := 1
  227. if chunkSize > 0 && task.fileSize > chunkSize {
  228. chunkCount = int(task.fileSize/chunkSize) + 1
  229. }
  230. if chunkCount == 1 {
  231. return worker.uploadFileAsOne(task, f)
  232. }
  233. return worker.uploadFileInChunks(task, f, chunkCount, chunkSize)
  234. }
  235. func (worker *FileCopyWorker) uploadFileAsOne(task FileCopyTask, f *os.File) error {
  236. // upload the file content
  237. fileName := filepath.Base(f.Name())
  238. mimeType := detectMimeType(f)
  239. data, err := ioutil.ReadAll(f)
  240. if err != nil {
  241. return err
  242. }
  243. var chunks []*filer_pb.FileChunk
  244. var assignResult *filer_pb.AssignVolumeResponse
  245. var assignError error
  246. if task.fileSize > 0 {
  247. // assign a volume
  248. err := pb.WithGrpcFilerClient(worker.filerGrpcAddress, worker.options.grpcDialOption, func(client filer_pb.SeaweedFilerClient) error {
  249. request := &filer_pb.AssignVolumeRequest{
  250. Count: 1,
  251. Replication: *worker.options.replication,
  252. Collection: *worker.options.collection,
  253. TtlSec: worker.options.ttlSec,
  254. ParentPath: task.destinationUrlPath,
  255. }
  256. assignResult, assignError = client.AssignVolume(context.Background(), request)
  257. if assignError != nil {
  258. return fmt.Errorf("assign volume failure %v: %v", request, assignError)
  259. }
  260. if assignResult.Error != "" {
  261. return fmt.Errorf("assign volume failure %v: %v", request, assignResult.Error)
  262. }
  263. return nil
  264. })
  265. if err != nil {
  266. return fmt.Errorf("Failed to assign from %v: %v\n", worker.options.masters, err)
  267. }
  268. targetUrl := "http://" + assignResult.Url + "/" + assignResult.FileId
  269. uploadResult, err := operation.UploadData(targetUrl, fileName, worker.options.cipher, data, false, mimeType, nil, security.EncodedJwt(assignResult.Auth))
  270. if err != nil {
  271. return fmt.Errorf("upload data %v to %s: %v\n", fileName, targetUrl, err)
  272. }
  273. if uploadResult.Error != "" {
  274. return fmt.Errorf("upload %v to %s result: %v\n", fileName, targetUrl, uploadResult.Error)
  275. }
  276. fmt.Printf("uploaded %s to %s\n", fileName, targetUrl)
  277. chunks = append(chunks, uploadResult.ToPbFileChunk(assignResult.FileId, 0))
  278. fmt.Printf("copied %s => http://%s%s%s\n", fileName, worker.filerHost, task.destinationUrlPath, fileName)
  279. }
  280. if err := pb.WithGrpcFilerClient(worker.filerGrpcAddress, worker.options.grpcDialOption, func(client filer_pb.SeaweedFilerClient) error {
  281. request := &filer_pb.CreateEntryRequest{
  282. Directory: task.destinationUrlPath,
  283. Entry: &filer_pb.Entry{
  284. Name: fileName,
  285. Attributes: &filer_pb.FuseAttributes{
  286. Crtime: time.Now().Unix(),
  287. Mtime: time.Now().Unix(),
  288. Gid: task.gid,
  289. Uid: task.uid,
  290. FileSize: uint64(task.fileSize),
  291. FileMode: uint32(task.fileMode),
  292. Mime: mimeType,
  293. Replication: *worker.options.replication,
  294. Collection: *worker.options.collection,
  295. TtlSec: worker.options.ttlSec,
  296. },
  297. Chunks: chunks,
  298. },
  299. }
  300. if err := filer_pb.CreateEntry(client, request); err != nil {
  301. return fmt.Errorf("update fh: %v", err)
  302. }
  303. return nil
  304. }); err != nil {
  305. return fmt.Errorf("upload data %v to http://%s%s%s: %v\n", fileName, worker.filerHost, task.destinationUrlPath, fileName, err)
  306. }
  307. return nil
  308. }
  309. func (worker *FileCopyWorker) uploadFileInChunks(task FileCopyTask, f *os.File, chunkCount int, chunkSize int64) error {
  310. fileName := filepath.Base(f.Name())
  311. mimeType := detectMimeType(f)
  312. chunksChan := make(chan *filer_pb.FileChunk, chunkCount)
  313. concurrentChunks := make(chan struct{}, *worker.options.concurrenctChunks)
  314. var wg sync.WaitGroup
  315. var uploadError error
  316. var collection, replication string
  317. fmt.Printf("uploading %s in %d chunks ...\n", fileName, chunkCount)
  318. for i := int64(0); i < int64(chunkCount) && uploadError == nil; i++ {
  319. wg.Add(1)
  320. concurrentChunks <- struct{}{}
  321. go func(i int64) {
  322. defer func() {
  323. wg.Done()
  324. <-concurrentChunks
  325. }()
  326. // assign a volume
  327. var assignResult *filer_pb.AssignVolumeResponse
  328. var assignError error
  329. err := pb.WithGrpcFilerClient(worker.filerGrpcAddress, worker.options.grpcDialOption, func(client filer_pb.SeaweedFilerClient) error {
  330. request := &filer_pb.AssignVolumeRequest{
  331. Count: 1,
  332. Replication: *worker.options.replication,
  333. Collection: *worker.options.collection,
  334. TtlSec: worker.options.ttlSec,
  335. ParentPath: task.destinationUrlPath,
  336. }
  337. assignResult, assignError = client.AssignVolume(context.Background(), request)
  338. if assignError != nil {
  339. return fmt.Errorf("assign volume failure %v: %v", request, assignError)
  340. }
  341. if assignResult.Error != "" {
  342. return fmt.Errorf("assign volume failure %v: %v", request, assignResult.Error)
  343. }
  344. return nil
  345. })
  346. if err != nil {
  347. fmt.Printf("Failed to assign from %v: %v\n", worker.options.masters, err)
  348. }
  349. if err != nil {
  350. fmt.Printf("Failed to assign from %v: %v\n", worker.options.masters, err)
  351. }
  352. targetUrl := "http://" + assignResult.Url + "/" + assignResult.FileId
  353. if collection == "" {
  354. collection = assignResult.Collection
  355. }
  356. if replication == "" {
  357. replication = assignResult.Replication
  358. }
  359. uploadResult, err, _ := operation.Upload(targetUrl, fileName+"-"+strconv.FormatInt(i+1, 10), worker.options.cipher, io.NewSectionReader(f, i*chunkSize, chunkSize), false, "", nil, security.EncodedJwt(assignResult.Auth))
  360. if err != nil {
  361. uploadError = fmt.Errorf("upload data %v to %s: %v\n", fileName, targetUrl, err)
  362. return
  363. }
  364. if uploadResult.Error != "" {
  365. uploadError = fmt.Errorf("upload %v to %s result: %v\n", fileName, targetUrl, uploadResult.Error)
  366. return
  367. }
  368. chunksChan <- uploadResult.ToPbFileChunk(assignResult.FileId, i*chunkSize)
  369. fmt.Printf("uploaded %s-%d to %s [%d,%d)\n", fileName, i+1, targetUrl, i*chunkSize, i*chunkSize+int64(uploadResult.Size))
  370. }(i)
  371. }
  372. wg.Wait()
  373. close(chunksChan)
  374. var chunks []*filer_pb.FileChunk
  375. for chunk := range chunksChan {
  376. chunks = append(chunks, chunk)
  377. }
  378. if uploadError != nil {
  379. var fileIds []string
  380. for _, chunk := range chunks {
  381. fileIds = append(fileIds, chunk.FileId)
  382. }
  383. operation.DeleteFiles(copy.masters[0], false, worker.options.grpcDialOption, fileIds)
  384. return uploadError
  385. }
  386. if err := pb.WithGrpcFilerClient(worker.filerGrpcAddress, worker.options.grpcDialOption, func(client filer_pb.SeaweedFilerClient) error {
  387. request := &filer_pb.CreateEntryRequest{
  388. Directory: task.destinationUrlPath,
  389. Entry: &filer_pb.Entry{
  390. Name: fileName,
  391. Attributes: &filer_pb.FuseAttributes{
  392. Crtime: time.Now().Unix(),
  393. Mtime: time.Now().Unix(),
  394. Gid: task.gid,
  395. Uid: task.uid,
  396. FileSize: uint64(task.fileSize),
  397. FileMode: uint32(task.fileMode),
  398. Mime: mimeType,
  399. Replication: replication,
  400. Collection: collection,
  401. TtlSec: worker.options.ttlSec,
  402. },
  403. Chunks: chunks,
  404. },
  405. }
  406. if err := filer_pb.CreateEntry(client, request); err != nil {
  407. return fmt.Errorf("update fh: %v", err)
  408. }
  409. return nil
  410. }); err != nil {
  411. return fmt.Errorf("upload data %v to http://%s%s%s: %v\n", fileName, worker.filerHost, task.destinationUrlPath, fileName, err)
  412. }
  413. fmt.Printf("copied %s => http://%s%s%s\n", fileName, worker.filerHost, task.destinationUrlPath, fileName)
  414. return nil
  415. }
  416. func detectMimeType(f *os.File) string {
  417. head := make([]byte, 512)
  418. f.Seek(0, io.SeekStart)
  419. n, err := f.Read(head)
  420. if err == io.EOF {
  421. return ""
  422. }
  423. if err != nil {
  424. fmt.Printf("read head of %v: %v\n", f.Name(), err)
  425. return ""
  426. }
  427. f.Seek(0, io.SeekStart)
  428. mimeType := http.DetectContentType(head[:n])
  429. if mimeType == "application/octet-stream" {
  430. return ""
  431. }
  432. return mimeType
  433. }