filer_copy.go 15 KB

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