fileOpr.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764
  1. package filesystem
  2. /*
  3. File Operation Wrapper
  4. author: tobychui
  5. This is a module seperated from the aroz online file system script
  6. that allows cleaner code in the main logic handler of the aroz online system.
  7. WARNING! ALL FILE OPERATION USING THIS WRAPPER SHOULD PASS IN REALPATH
  8. DO NOT USE VIRTUAL PATH FOR ANY OPERATIONS WITH THIS WRAPPER
  9. */
  10. import (
  11. "archive/zip"
  12. "compress/flate"
  13. "errors"
  14. "fmt"
  15. "io"
  16. "log"
  17. "os"
  18. "path/filepath"
  19. "strconv"
  20. "strings"
  21. "time"
  22. "imuslab.com/arozos/mod/filesystem/hidden"
  23. archiver "github.com/mholt/archiver/v3"
  24. )
  25. //A basic file zipping function
  26. func ZipFile(filelist []string, outputfile string, includeTopLevelFolder bool) error {
  27. z := archiver.Zip{
  28. CompressionLevel: flate.DefaultCompression,
  29. MkdirAll: true,
  30. SelectiveCompression: true,
  31. OverwriteExisting: false,
  32. ImplicitTopLevelFolder: includeTopLevelFolder,
  33. }
  34. err := z.Archive(filelist, outputfile)
  35. return err
  36. }
  37. //A basic file unzip function
  38. func Unzip(source, destination string) error {
  39. archive, err := zip.OpenReader(source)
  40. if err != nil {
  41. return err
  42. }
  43. defer archive.Close()
  44. for _, file := range archive.Reader.File {
  45. reader, err := file.Open()
  46. if err != nil {
  47. return err
  48. }
  49. defer reader.Close()
  50. path := filepath.Join(destination, file.Name)
  51. err = os.MkdirAll(path, os.ModePerm)
  52. if err != nil {
  53. return err
  54. }
  55. if file.FileInfo().IsDir() {
  56. continue
  57. }
  58. err = os.Remove(path)
  59. if err != nil {
  60. return err
  61. }
  62. writer, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
  63. if err != nil {
  64. return err
  65. }
  66. defer writer.Close()
  67. _, err = io.Copy(writer, reader)
  68. if err != nil {
  69. return err
  70. }
  71. }
  72. return nil
  73. }
  74. //Aroz Unzip File with progress update function (current filename / current file count / total file count / progress in percentage)
  75. func ArozUnzipFileWithProgress(filelist []string, outputfile string, progressHandler func(string, int, int, float64)) error {
  76. //Gether the total number of files in all zip files
  77. totalFileCounts := 0
  78. unzippedFileCount := 0
  79. for _, srcFile := range filelist {
  80. archive, err := zip.OpenReader(srcFile)
  81. if err != nil {
  82. return err
  83. }
  84. totalFileCounts += len(archive.Reader.File)
  85. archive.Close()
  86. }
  87. //Start extracting
  88. for _, srcFile := range filelist {
  89. archive, err := zip.OpenReader(srcFile)
  90. if err != nil {
  91. return err
  92. }
  93. defer archive.Close()
  94. for _, file := range archive.Reader.File {
  95. reader, err := file.Open()
  96. if err != nil {
  97. return err
  98. }
  99. defer reader.Close()
  100. path := filepath.Join(outputfile, file.Name)
  101. err = os.MkdirAll(path, os.ModePerm)
  102. if err != nil {
  103. return err
  104. }
  105. if file.FileInfo().IsDir() {
  106. //Folder extracted
  107. //Update the progress
  108. unzippedFileCount++
  109. progressHandler(file.Name, unzippedFileCount, totalFileCounts, float64(unzippedFileCount)/float64(totalFileCounts)*100.0)
  110. continue
  111. }
  112. err = os.Remove(path)
  113. if err != nil {
  114. return err
  115. }
  116. writer, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
  117. if err != nil {
  118. return err
  119. }
  120. defer writer.Close()
  121. _, err = io.Copy(writer, reader)
  122. if err != nil {
  123. return err
  124. }
  125. //Update the progress
  126. unzippedFileCount++
  127. progressHandler(file.Name, unzippedFileCount, totalFileCounts, float64(unzippedFileCount)/float64(totalFileCounts)*100.0)
  128. }
  129. }
  130. return nil
  131. }
  132. //Aroz Zip File with progress update function (current filename / current file count / total file count / progress in percentage)
  133. func ArozZipFileWithProgress(filelist []string, outputfile string, includeTopLevelFolder bool, progressHandler func(string, int, int, float64)) error {
  134. //Get the file count from the filelist
  135. totalFileCount := 0
  136. for _, srcpath := range filelist {
  137. if IsDir(srcpath) {
  138. filepath.Walk(srcpath, func(_ string, info os.FileInfo, _ error) error {
  139. if !info.IsDir() {
  140. totalFileCount++
  141. }
  142. return nil
  143. })
  144. } else {
  145. totalFileCount++
  146. }
  147. }
  148. //Create the target zip file
  149. file, err := os.Create(outputfile)
  150. if err != nil {
  151. panic(err)
  152. }
  153. defer file.Close()
  154. writer := zip.NewWriter(file)
  155. defer writer.Close()
  156. currentFileCount := 0
  157. for _, srcpath := range filelist {
  158. if IsDir(srcpath) {
  159. //This is a directory
  160. topLevelFolderName := filepath.ToSlash(filepath.Base(filepath.Dir(srcpath)) + "/" + filepath.Base(srcpath))
  161. err = filepath.Walk(srcpath, func(path string, info os.FileInfo, err error) error {
  162. if err != nil {
  163. return err
  164. }
  165. if info.IsDir() {
  166. return nil
  167. }
  168. if insideHiddenFolder(path) == true {
  169. //This is hidden file / folder. Skip this
  170. return nil
  171. }
  172. file, err := os.Open(path)
  173. if err != nil {
  174. return err
  175. }
  176. defer file.Close()
  177. relativePath := strings.ReplaceAll(filepath.ToSlash(path), filepath.ToSlash(filepath.Clean(srcpath))+"/", "")
  178. if includeTopLevelFolder {
  179. relativePath = topLevelFolderName + "/" + relativePath
  180. } else {
  181. relativePath = filepath.Base(srcpath) + "/" + relativePath
  182. }
  183. f, err := writer.Create(relativePath)
  184. if err != nil {
  185. return err
  186. }
  187. _, err = io.Copy(f, file)
  188. if err != nil {
  189. return err
  190. }
  191. //Update the zip progress
  192. currentFileCount++
  193. progressHandler(filepath.Base(srcpath), currentFileCount, totalFileCount, (float64(currentFileCount)/float64(totalFileCount))*float64(100))
  194. return nil
  195. })
  196. if err != nil {
  197. return err
  198. }
  199. } else {
  200. //This is a file
  201. topLevelFolderName := filepath.Base(filepath.Dir(srcpath))
  202. file, err := os.Open(srcpath)
  203. if err != nil {
  204. return err
  205. }
  206. defer file.Close()
  207. relativePath := filepath.Base(srcpath)
  208. if includeTopLevelFolder {
  209. relativePath = topLevelFolderName + "/" + relativePath
  210. }
  211. f, err := writer.Create(relativePath)
  212. if err != nil {
  213. return err
  214. }
  215. _, err = io.Copy(f, file)
  216. if err != nil {
  217. return err
  218. }
  219. //Update the zip progress
  220. currentFileCount++
  221. progressHandler(filepath.Base(srcpath), currentFileCount, totalFileCount, (float64(currentFileCount)/float64(totalFileCount))*float64(100))
  222. }
  223. }
  224. return nil
  225. }
  226. //ArOZ Zip FIle, but with no progress display
  227. func ArozZipFile(filelist []string, outputfile string, includeTopLevelFolder bool) error {
  228. //Create the target zip file
  229. file, err := os.Create(outputfile)
  230. if err != nil {
  231. return err
  232. }
  233. defer file.Close()
  234. writer := zip.NewWriter(file)
  235. defer writer.Close()
  236. for _, srcpath := range filelist {
  237. if IsDir(srcpath) {
  238. //This is a directory
  239. topLevelFolderName := filepath.ToSlash(filepath.Base(filepath.Dir(srcpath)) + "/" + filepath.Base(srcpath))
  240. err = filepath.Walk(srcpath, func(path string, info os.FileInfo, err error) error {
  241. if err != nil {
  242. return err
  243. }
  244. if info.IsDir() {
  245. return nil
  246. }
  247. if insideHiddenFolder(path) == true {
  248. //This is hidden file / folder. Skip this
  249. return nil
  250. }
  251. file, err := os.Open(path)
  252. if err != nil {
  253. return err
  254. }
  255. defer file.Close()
  256. relativePath := strings.ReplaceAll(filepath.ToSlash(path), filepath.ToSlash(filepath.Clean(srcpath))+"/", "")
  257. if includeTopLevelFolder {
  258. relativePath = topLevelFolderName + "/" + relativePath
  259. } else {
  260. relativePath = filepath.Base(srcpath) + "/" + relativePath
  261. }
  262. f, err := writer.Create(relativePath)
  263. if err != nil {
  264. return err
  265. }
  266. _, err = io.Copy(f, file)
  267. if err != nil {
  268. return err
  269. }
  270. return nil
  271. })
  272. if err != nil {
  273. return err
  274. }
  275. } else {
  276. //This is a file
  277. topLevelFolderName := filepath.Base(filepath.Dir(srcpath))
  278. file, err := os.Open(srcpath)
  279. if err != nil {
  280. return err
  281. }
  282. defer file.Close()
  283. relativePath := filepath.Base(srcpath)
  284. if includeTopLevelFolder {
  285. relativePath = topLevelFolderName + "/" + relativePath
  286. }
  287. f, err := writer.Create(relativePath)
  288. if err != nil {
  289. return err
  290. }
  291. _, err = io.Copy(f, file)
  292. if err != nil {
  293. return err
  294. }
  295. }
  296. }
  297. return nil
  298. }
  299. func insideHiddenFolder(path string) bool {
  300. FileIsHidden, err := hidden.IsHidden(path, true)
  301. if err != nil {
  302. //Read error. Maybe permission issue, assuem is hidden
  303. return true
  304. }
  305. return FileIsHidden
  306. /*
  307. thisPathInfo := filepath.ToSlash(filepath.Clean(path))
  308. pathData := strings.Split(thisPathInfo, "/")
  309. for _, thispd := range pathData {
  310. if len(thispd) > 0 && thispd[:1] == "." {
  311. //This path contain one of the folder is hidden
  312. return true
  313. }
  314. }
  315. return false
  316. */
  317. }
  318. func ViewZipFile(filepath string) ([]string, error) {
  319. z := archiver.Zip{}
  320. filelist := []string{}
  321. err := z.Walk(filepath, func(f archiver.File) error {
  322. filelist = append(filelist, f.Name())
  323. return nil
  324. })
  325. return filelist, err
  326. }
  327. func FileCopy(src string, dest string, mode string, progressUpdate func(int, string)) error {
  328. srcRealpath, _ := filepath.Abs(src)
  329. destRealpath, _ := filepath.Abs(dest)
  330. if IsDir(src) && strings.Contains(filepath.ToSlash(destRealpath)+"/", filepath.ToSlash(srcRealpath)+"/") {
  331. //Recursive operation. Reject
  332. return errors.New("Recursive copy operation.")
  333. }
  334. //Check if the copy destination file already have an identical file
  335. copiedFilename := filepath.Base(src)
  336. if fileExists(dest + filepath.Base(src)) {
  337. if mode == "" {
  338. //Do not specific file exists principle
  339. return errors.New("Destination file already exists.")
  340. } else if mode == "skip" {
  341. //Skip this file
  342. return nil
  343. } else if mode == "overwrite" {
  344. //Continue with the following code
  345. //Check if the copy and paste dest are identical
  346. if src == (dest + filepath.Base(src)) {
  347. //Source and target identical. Cannot overwrite.
  348. return errors.New("Source and destination paths are identical.")
  349. }
  350. } else if mode == "keep" {
  351. //Keep the file but saved with 'Copy' suffix
  352. newFilename := strings.TrimSuffix(filepath.Base(src), filepath.Ext(src)) + " - Copy" + filepath.Ext(src)
  353. //Check if the newFilename already exists. If yes, continue adding suffix
  354. duplicateCounter := 0
  355. for fileExists(dest + newFilename) {
  356. duplicateCounter++
  357. newFilename = strings.TrimSuffix(filepath.Base(src), filepath.Ext(src)) + " - Copy(" + strconv.Itoa(duplicateCounter) + ")" + filepath.Ext(src)
  358. if duplicateCounter > 1024 {
  359. //Maxmium loop encountered. For thread safty, terminate here
  360. return errors.New("Too many copies of identical files.")
  361. }
  362. }
  363. copiedFilename = newFilename
  364. } else {
  365. //This exists opr not supported.
  366. return errors.New("Unknown file exists rules given.")
  367. }
  368. }
  369. //Fix the lacking / at the end if true
  370. if dest[len(dest)-1:] != "/" {
  371. dest = dest + "/"
  372. }
  373. //Ready to move. Check if both folder are located in the same root devices. If not, use copy and delete method.
  374. if IsDir(src) {
  375. //Source file is directory. CopyFolder
  376. realDest := dest + copiedFilename
  377. //err := dircpy.Copy(src, realDest)
  378. err := dirCopy(src, realDest, progressUpdate)
  379. if err != nil {
  380. return err
  381. }
  382. } else {
  383. //Source is file only. Copy file.
  384. realDest := dest + copiedFilename
  385. source, err := os.Open(src)
  386. if err != nil {
  387. return err
  388. }
  389. destination, err := os.Create(realDest)
  390. if err != nil {
  391. return err
  392. }
  393. if progressUpdate != nil {
  394. //Set progress to 100, leave it to upper level abstraction to handle
  395. progressUpdate(100, filepath.Base(realDest))
  396. }
  397. _, err = io.Copy(destination, source)
  398. if err != nil {
  399. return err
  400. }
  401. source.Close()
  402. destination.Close()
  403. }
  404. return nil
  405. }
  406. func FileMove(src string, dest string, mode string, fastMove bool, progressUpdate func(int, string)) error {
  407. srcRealpath, _ := filepath.Abs(src)
  408. destRealpath, _ := filepath.Abs(dest)
  409. if IsDir(src) && strings.Contains(filepath.ToSlash(destRealpath)+"/", filepath.ToSlash(srcRealpath)+"/") {
  410. //Recursive operation. Reject
  411. return errors.New("Recursive move operation.")
  412. }
  413. if !fileExists(dest) {
  414. if fileExists(filepath.Dir(dest)) {
  415. //User pass in the whole path for the folder. Report error usecase.
  416. return errors.New("Dest location should be an existing folder instead of the full path of the moved file.")
  417. }
  418. return errors.New("Dest folder not found")
  419. }
  420. //Fix the lacking / at the end if true
  421. if dest[len(dest)-1:] != "/" {
  422. dest = dest + "/"
  423. }
  424. //Check if the target file already exists.
  425. movedFilename := filepath.Base(src)
  426. if fileExists(dest + filepath.Base(src)) {
  427. //Handle cases where file already exists
  428. if mode == "" {
  429. //Do not specific file exists principle
  430. return errors.New("Destination file already exists.")
  431. } else if mode == "skip" {
  432. //Skip this file
  433. return nil
  434. } else if mode == "overwrite" {
  435. //Continue with the following code
  436. //Check if the copy and paste dest are identical
  437. if src == (dest + filepath.Base(src)) {
  438. //Source and target identical. Cannot overwrite.
  439. return errors.New("Source and destination paths are identical.")
  440. }
  441. } else if mode == "keep" {
  442. //Keep the file but saved with 'Copy' suffix
  443. newFilename := strings.TrimSuffix(filepath.Base(src), filepath.Ext(src)) + " - Copy" + filepath.Ext(src)
  444. //Check if the newFilename already exists. If yes, continue adding suffix
  445. duplicateCounter := 0
  446. for fileExists(dest + newFilename) {
  447. duplicateCounter++
  448. newFilename = strings.TrimSuffix(filepath.Base(src), filepath.Ext(src)) + " - Copy(" + strconv.Itoa(duplicateCounter) + ")" + filepath.Ext(src)
  449. if duplicateCounter > 1024 {
  450. //Maxmium loop encountered. For thread safty, terminate here
  451. return errors.New("Too many copies of identical files.")
  452. }
  453. }
  454. movedFilename = newFilename
  455. } else {
  456. //This exists opr not supported.
  457. return errors.New("Unknown file exists rules given.")
  458. }
  459. }
  460. if fastMove {
  461. //Ready to move with the quick rename method
  462. realDest := dest + movedFilename
  463. err := os.Rename(src, realDest)
  464. if err != nil {
  465. log.Println(err)
  466. return errors.New("File Move Failed")
  467. }
  468. } else {
  469. //Ready to move. Check if both folder are located in the same root devices. If not, use copy and delete method.
  470. if IsDir(src) {
  471. //Source file is directory. CopyFolder
  472. realDest := dest + movedFilename
  473. //err := dircpy.Copy(src, realDest)
  474. err := dirCopy(src, realDest, progressUpdate)
  475. if err != nil {
  476. return err
  477. } else {
  478. //Move completed. Remove source file.
  479. os.RemoveAll(src)
  480. return nil
  481. }
  482. } else {
  483. //Source is file only. Copy file.
  484. realDest := dest + movedFilename
  485. /*
  486. Updates 20-10-2020, replaced io.Copy to BufferedLargeFileCopy
  487. Legacy code removed.
  488. */
  489. //Update the progress
  490. if progressUpdate != nil {
  491. progressUpdate(100, filepath.Base(src))
  492. }
  493. err := BufferedLargeFileCopy(src, realDest, 8192)
  494. if err != nil {
  495. log.Println("BLFC error: ", err.Error())
  496. return err
  497. }
  498. //Delete the source file after copy
  499. err = os.Remove(src)
  500. counter := 0
  501. for err != nil {
  502. //Sometime Windows need this to prevent windows caching bring problems to file remove
  503. time.Sleep(1 * time.Second)
  504. os.Remove(src)
  505. counter++
  506. log.Println("Retrying to remove file: " + src)
  507. if counter > 10 {
  508. return errors.New("Source file remove failed.")
  509. }
  510. }
  511. }
  512. }
  513. return nil
  514. }
  515. //Copy a given directory, with no progress udpate
  516. func CopyDir(src string, dest string) error {
  517. return dirCopy(src, dest, func(progress int, name string) {})
  518. }
  519. //Replacment of the legacy dirCopy plugin with filepath.Walk function. Allowing real time progress update to front end
  520. func dirCopy(src string, realDest string, progressUpdate func(int, string)) error {
  521. //Get the total file counts
  522. totalFileCounts := int64(0)
  523. filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
  524. if !info.IsDir() {
  525. //Updates 22 April 2021, chnaged from file count to file size for progress update
  526. //totalFileCounts++
  527. totalFileCounts += info.Size()
  528. }
  529. return nil
  530. })
  531. //Make the destinaton directory
  532. if !fileExists(realDest) {
  533. os.Mkdir(realDest, 0755)
  534. }
  535. //Start moving
  536. fileCounter := int64(0)
  537. err := filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
  538. srcAbs, _ := filepath.Abs(src)
  539. pathAbs, _ := filepath.Abs(path)
  540. var folderRootRelative string = strings.Replace(pathAbs, srcAbs, "", 1)
  541. if folderRootRelative == "" {
  542. return nil
  543. }
  544. if info.IsDir() {
  545. //Mkdir base on relative path
  546. return os.MkdirAll(filepath.Join(realDest, folderRootRelative), 0755)
  547. } else {
  548. //fileCounter++
  549. fileCounter += info.Size()
  550. //Move file base on relative path
  551. fileSrc := filepath.ToSlash(filepath.Join(filepath.Clean(src), folderRootRelative))
  552. fileDest := filepath.ToSlash(filepath.Join(filepath.Clean(realDest), folderRootRelative))
  553. //Update move progress
  554. if progressUpdate != nil {
  555. progressUpdate(int(float64(fileCounter)/float64(totalFileCounts)*100), filepath.Base(fileSrc))
  556. }
  557. //Move the file using BLFC
  558. err := BufferedLargeFileCopy(fileSrc, fileDest, 8192)
  559. if err != nil {
  560. //Ignore and continue
  561. log.Println("BLFC Error:", err.Error())
  562. return nil
  563. }
  564. /*
  565. //Move fiel using IO Copy
  566. err := BasicFileCopy(fileSrc, fileDest)
  567. if err != nil {
  568. log.Println("Basic Copy Error: ", err.Error())
  569. return nil
  570. }
  571. */
  572. }
  573. return nil
  574. })
  575. return err
  576. }
  577. func BasicFileCopy(src string, dst string) error {
  578. sourceFileStat, err := os.Stat(src)
  579. if err != nil {
  580. return err
  581. }
  582. if !sourceFileStat.Mode().IsRegular() {
  583. return fmt.Errorf("%s is not a regular file", src)
  584. }
  585. source, err := os.Open(src)
  586. if err != nil {
  587. return err
  588. }
  589. defer source.Close()
  590. destination, err := os.Create(dst)
  591. if err != nil {
  592. return err
  593. }
  594. defer destination.Close()
  595. _, err = io.Copy(destination, source)
  596. return err
  597. }
  598. //Use for copying large file using buffering method. Allowing copying large file with little RAM
  599. func BufferedLargeFileCopy(src string, dst string, BUFFERSIZE int64) error {
  600. sourceFileStat, err := os.Stat(src)
  601. if err != nil {
  602. return err
  603. }
  604. if !sourceFileStat.Mode().IsRegular() {
  605. return errors.New("Invalid file source")
  606. }
  607. source, err := os.Open(src)
  608. if err != nil {
  609. return err
  610. }
  611. destination, err := os.Create(dst)
  612. if err != nil {
  613. return err
  614. }
  615. buf := make([]byte, BUFFERSIZE)
  616. for {
  617. n, err := source.Read(buf)
  618. if err != nil && err != io.EOF {
  619. source.Close()
  620. destination.Close()
  621. return err
  622. }
  623. if n == 0 {
  624. source.Close()
  625. destination.Close()
  626. break
  627. }
  628. if _, err := destination.Write(buf[:n]); err != nil {
  629. source.Close()
  630. destination.Close()
  631. return err
  632. }
  633. }
  634. return nil
  635. }
  636. func IsDir(path string) bool {
  637. if fileExists(path) == false {
  638. return false
  639. }
  640. fi, err := os.Stat(path)
  641. if err != nil {
  642. log.Fatal(err)
  643. return false
  644. }
  645. switch mode := fi.Mode(); {
  646. case mode.IsDir():
  647. return true
  648. case mode.IsRegular():
  649. return false
  650. }
  651. return false
  652. }