66 "crypto/sha1" //nolint:gosec
77 "crypto/sha256"
88 "crypto/sha512"
9+ "encoding/csv"
910 "encoding/hex"
11+ "errors"
1012 "fmt"
1113 "hash"
1214 "hash/crc32"
@@ -39,7 +41,6 @@ func (c *clientHandler) transferFile(write bool, append bool) {
3941 var file FileTransfer
4042 var err error
4143 var fileFlag int
42- var filePerm os.FileMode = 0777
4344
4445 path := c .absPath (c .param )
4546
@@ -60,11 +61,7 @@ func (c *clientHandler) transferFile(write bool, append bool) {
6061 fileFlag = os .O_RDONLY
6162 }
6263
63- if fileTransfer , ok := c .driver .(ClientDriverExtentionFileTransfer ); ok {
64- file , err = fileTransfer .GetHandle (path , fileFlag , c .ctxRest )
65- } else {
66- file , err = c .driver .OpenFile (path , fileFlag , filePerm )
67- }
64+ file , err = c .getFileHandle (path , fileFlag , c .ctxRest )
6865
6966 // If this fail, can stop right here and reset the seek position
7067 if err != nil {
@@ -84,7 +81,7 @@ func (c *clientHandler) transferFile(write bool, append bool) {
8481 // if we are unable to seek we can stop right here and close the file
8582 c .writeMessage (StatusActionNotTaken , "Could not seek file: " + err .Error ())
8683 // we can ignore the close error here
87- file . Close () //nolint:errcheck,gosec
84+ c . closeUnchecked ( file )
8885
8986 return
9087 }
@@ -94,7 +91,7 @@ func (c *clientHandler) transferFile(write bool, append bool) {
9491 if err != nil {
9592 // an error is already returned to the FTP client
9693 // we can stop right here and close the file ignoring close error if any
97- file . Close () //nolint:errcheck,gosec
94+ c . closeUnchecked ( file )
9895
9996 return
10097 }
@@ -144,6 +141,92 @@ func (c *clientHandler) doFileTransfer(tr net.Conn, file io.ReadWriter, write bo
144141 return err
145142}
146143
144+ func (c * clientHandler ) handleCOMB () error {
145+ if ! c .server .settings .EnableCOMB {
146+ // if disabled the client should not arrive here as COMB support is not declared in the FEAT response
147+ c .writeMessage (StatusCommandNotImplemented , "COMB support is disabled" )
148+ return nil
149+ }
150+
151+ relativePaths , err := unquoteSpaceSeparatedParams (c .param )
152+ if err != nil || len (relativePaths ) < 2 {
153+ c .writeMessage (StatusSyntaxErrorParameters , fmt .Sprintf ("invalid COMB parameters: %v" , c .param ))
154+ return nil
155+ }
156+
157+ targetPath := c .absPath (relativePaths [0 ])
158+
159+ sourcePaths := make ([]string , 0 , len (relativePaths )- 1 )
160+ for _ , src := range relativePaths [1 :] {
161+ sourcePaths = append (sourcePaths , c .absPath (src ))
162+ }
163+ // if targetPath exists we have append to it
164+ // partial files will be deleted if COMB succeeded
165+ _ , err = c .driver .Stat (targetPath )
166+ if err != nil && ! errors .Is (err , os .ErrNotExist ) {
167+ c .writeMessage (StatusActionNotTaken , fmt .Sprintf ("Could not access file %#v: %v" , targetPath , err ))
168+ return nil
169+ }
170+
171+ fileFlag := os .O_WRONLY
172+ if errors .Is (err , os .ErrNotExist ) {
173+ fileFlag |= os .O_CREATE
174+ } else {
175+ fileFlag |= os .O_APPEND
176+ }
177+
178+ c .combineFiles (targetPath , fileFlag , sourcePaths )
179+
180+ return nil
181+ }
182+
183+ func (c * clientHandler ) combineFiles (targetPath string , fileFlag int , sourcePaths []string ) {
184+ file , err := c .getFileHandle (targetPath , fileFlag , 0 )
185+ if err != nil {
186+ c .writeMessage (StatusActionNotTaken , fmt .Sprintf ("Could not access file %#v: %v" , targetPath , err ))
187+ return
188+ }
189+
190+ for _ , partial := range sourcePaths {
191+ var src FileTransfer
192+
193+ src , err = c .getFileHandle (partial , os .O_RDONLY , 0 )
194+ if err != nil {
195+ c .closeUnchecked (file )
196+ c .writeMessage (StatusActionNotTaken , fmt .Sprintf ("Could not access file %#v: %v" , partial , err ))
197+
198+ return
199+ }
200+
201+ _ , err = io .Copy (file , src )
202+ if err != nil {
203+ c .closeUnchecked (src )
204+ c .closeUnchecked (file )
205+ c .writeMessage (StatusActionNotTaken , fmt .Sprintf ("Could combine file %#v: %v" , partial , err ))
206+
207+ return
208+ }
209+
210+ c .closeUnchecked (src )
211+
212+ err = c .driver .Remove (partial )
213+ if err != nil {
214+ c .closeUnchecked (file )
215+ c .writeMessage (StatusActionNotTaken , fmt .Sprintf ("Could delete file %#v after combine: %v" , partial , err ))
216+
217+ return
218+ }
219+ }
220+
221+ err = file .Close ()
222+ if err != nil {
223+ c .writeMessage (StatusActionNotTaken , fmt .Sprintf ("Could close combined file %#v: %v" , targetPath , err ))
224+ return
225+ }
226+
227+ c .writeMessage (StatusFileOK , "COMB succeeded!" )
228+ }
229+
147230func (c * clientHandler ) handleCHMOD (params string ) {
148231 spl := strings .SplitN (params , " " , 2 )
149232 modeNb , err := strconv .ParseUint (spl [0 ], 8 , 32 )
@@ -435,6 +518,12 @@ func (c *clientHandler) handleSHA512() error {
435518}
436519
437520func (c * clientHandler ) handleGenericHash (algo HASHAlgo , isCustomMode bool ) error {
521+ if ! c .server .settings .EnableHASH {
522+ // if disabled the client should not arrive here as HASH support is not declared in the FEAT response
523+ c .writeMessage (StatusCommandNotImplemented , "File hash support is disabled" )
524+ return nil
525+ }
526+
438527 args := strings .SplitN (c .param , " " , 3 )
439528 info , err := c .driver .Stat (args [0 ])
440529
@@ -451,6 +540,9 @@ func (c *clientHandler) handleGenericHash(algo HASHAlgo, isCustomMode bool) erro
451540 start := int64 (0 )
452541 end := info .Size ()
453542
543+ // to support partial hash also for the HASH command we should implement RANG too,
544+ // but this apply also to uploads/downloads and so complicat the things, we'll add
545+ // this support in future improvements
454546 if isCustomMode {
455547 // for custom command the range can be specified in this way:
456548 // XSHA1 <file> <start> <end>
@@ -470,9 +562,7 @@ func (c *clientHandler) handleGenericHash(algo HASHAlgo, isCustomMode bool) erro
470562 }
471563 }
472564 }
473- // to support partial hash also for the HASH command we should implement RANG too,
474- // but this apply also to uploads/downloads and so complicat the things, we'll add
475- // this support in future improvements
565+
476566 var result string
477567 if hasher , ok := c .driver .(ClientDriverExtensionHasher ); ok {
478568 result , err = hasher .ComputeHash (c .absPath (args [0 ]), algo , start , end )
@@ -485,15 +575,7 @@ func (c *clientHandler) handleGenericHash(algo HASHAlgo, isCustomMode bool) erro
485575 return nil
486576 }
487577
488- hashMapping := getHashMapping ()
489- hashName := ""
490-
491- for k , v := range hashMapping {
492- if v == algo {
493- hashName = k
494- }
495- }
496-
578+ hashName := getHashName (algo )
497579 firstLine := fmt .Sprintf ("Computing %v digest" , hashName )
498580
499581 if isCustomMode {
@@ -527,11 +609,7 @@ func (c *clientHandler) computeHashForFile(filePath string, algo HASHAlgo, start
527609 return "" , errUnknowHash
528610 }
529611
530- if fileTransfer , ok := c .driver .(ClientDriverExtentionFileTransfer ); ok {
531- file , err = fileTransfer .GetHandle (filePath , os .O_RDONLY , start )
532- } else {
533- file , err = c .driver .OpenFile (filePath , os .O_RDONLY , os .ModePerm )
534- }
612+ file , err = c .getFileHandle (filePath , os .O_RDONLY , start )
535613
536614 if err != nil {
537615 return "" , err
@@ -545,11 +623,43 @@ func (c *clientHandler) computeHashForFile(filePath string, algo HASHAlgo, start
545623 }
546624
547625 _ , err = io .CopyN (h , file , end - start )
548- defer file . Close () //nolint:errcheck // we ignore close error here
626+ defer c . closeUnchecked ( file ) // we ignore close error here
549627
550628 if err != nil && err != io .EOF {
551629 return "" , err
552630 }
553631
554632 return hex .EncodeToString (h .Sum (nil )), nil
555633}
634+
635+ func (c * clientHandler ) getFileHandle (name string , flags int , offset int64 ) (FileTransfer , error ) {
636+ if fileTransfer , ok := c .driver .(ClientDriverExtentionFileTransfer ); ok {
637+ return fileTransfer .GetHandle (name , flags , offset )
638+ }
639+
640+ return c .driver .OpenFile (name , flags , os .ModePerm )
641+ }
642+
643+ func (c * clientHandler ) closeUnchecked (file io.Closer ) {
644+ if err := file .Close (); err != nil {
645+ c .logger .Warn (
646+ "Problem closing a file" ,
647+ "err" , err ,
648+ )
649+ }
650+ }
651+
652+ // This method split params by spaces, expect when the space is inside quotes.
653+ // It was introduced to support COMB command. Supported COMB examples:
654+ //
655+ // - Append a single part onto an existing (or new) file: e.g., COMB "final.log" "132.log".
656+ // - Target and source files do not require enclosing quotes UNLESS the filename includes spaces:
657+ // - COMB final5.log 64.log 65.log
658+ // - COMB "final5.log" "64.log" "65.log"
659+ // - COMB final7.log "6 6.log" 67.log
660+ func unquoteSpaceSeparatedParams (params string ) ([]string , error ) {
661+ reader := csv .NewReader (strings .NewReader (params ))
662+ reader .Comma = ' ' // space
663+
664+ return reader .Read ()
665+ }
0 commit comments