Repository: pkg/sftp
Pull request #332 (2020-01-03 19:30:46)

Added a ClientOption to determine whetehr to use Fstat or Stat when F…
…ile.WriteTo is being called to support strange behaviour on some servers. See #331 


Patch:
diff --git a/client.go b/client.go
index 50c7dc3..0d09d2a 100644
--- a/client.go
+++ b/client.go
@@ -51,6 +51,30 @@ func MaxPacketChecked(size int) ClientOption {
 	}
 }
 
+// UseFstat sets whether to use Fstat or Stat when File.WriteTo is called
+// (usually when copying files).
+// Some servers limit the amount of open files and calling Stat after opening
+// the file will throw an error From the server. Setting this flag will call
+// Fstat instead of Stat which is suppose to be called on an open file handle.
+//
+// It has been found that that with IBM Sterling SFTP servers which have
+// "extractability" level set to 1 which means only 1 file can be opened at
+// any given time.
+//
+// If the server you are working with still has an issue with both Stat and
+// Fstat calls you can always open a file and read it until the end.
+//
+// Another reason to read the file until its end and Fstat doesn't work is
+// that in some servers, reading a full file will automatically delete the
+// file as some of these mainframes map the file to a message in a queue.
+// Once the file has been read it will get deleted.
+func UseFstat(value bool) ClientOption {
+	return func(c *Client) error {
+		c.useFstat = value
+		return nil
+	}
+}
+
 // MaxPacketUnchecked sets the maximum size of the payload, measured in bytes.
 // It accepts sizes larger than the 32768 bytes all servers should support.
 // Only use a setting higher than 32768 if your application always connects to
@@ -161,6 +185,7 @@ type Client struct {
 	maxPacket             int // max packet size read or written.
 	nextid                uint32
 	maxConcurrentRequests int
+	useFstat              bool
 }
 
 // Create creates the named file mode 0666 (before umask), truncating it if it
@@ -913,15 +938,26 @@ func (f *File) Read(b []byte) (int, error) {
 // maximise throughput for transferring the entire file (especially
 // over high latency links).
 func (f *File) WriteTo(w io.Writer) (int64, error) {
-	fi, err := f.c.Stat(f.path)
-	if err != nil {
-		return 0, err
+	var fileSize uint64
+	if f.c.useFstat {
+		fileStat, err := f.c.fstat(f.handle)
+		if err != nil {
+			return 0, err
+		}
+		fileSize = fileStat.Size
+
+	} else {
+		fi, err := f.c.Stat(f.path)
+		if err != nil {
+			return 0, err
+		}
+		fileSize = uint64(fi.Size())
 	}
+
 	inFlight := 0
 	desiredInFlight := 1
 	offset := f.offset
 	writeOffset := offset
-	fileSize := uint64(fi.Size())
 	// see comment on same line in Read() above
 	ch := make(chan result, f.c.maxConcurrentRequests+1)
 	type inflightRead struct {


Test patch:
diff --git a/client_test.go b/client_test.go
index c0d620d..05107c5 100644
--- a/client_test.go
+++ b/client_test.go
@@ -194,3 +194,17 @@ func testMaxPacketOption(t *testing.T, o ClientOption, tt packetSizeTest) {
 		t.Errorf("MaxPacketChecked(%v)\n- want: %v\n- got: %v", tt.size, tt.size, c.maxPacket)
 	}
 }
+
+func testFstatOption(t *testing.T, o ClientOption, value bool) {
+	var c Client
+
+	err := o(&c)
+	if err == nil && c.useFstat != value {
+		t.Errorf("UseFStat(%v)\n- want: %v\n- got: %v", value, value, c.useFstat)
+	}
+}
+
+func TestUseFstatChecked(t *testing.T) {
+	testFstatOption(t, UseFstat(true), true)
+	testFstatOption(t, UseFstat(false), false)
+}