From 98cf3616b9b3dbd6128082c7edd12c7f5a0db070 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 12 Sep 2023 13:15:14 -0400 Subject: [PATCH 1/7] fix(ftp/ds): ECONNRESET error for empty dataset contents Signed-off-by: Trae Yelovich --- .../zowe-explorer-ftp-extension/CHANGELOG.md | 2 ++ .../src/ZoweExplorerFtpMvsApi.ts | 20 ++++++++++++------- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/zowe-explorer-ftp-extension/CHANGELOG.md b/packages/zowe-explorer-ftp-extension/CHANGELOG.md index 3b940ff212..121eacc766 100644 --- a/packages/zowe-explorer-ftp-extension/CHANGELOG.md +++ b/packages/zowe-explorer-ftp-extension/CHANGELOG.md @@ -6,6 +6,8 @@ All notable changes to the "zowe-explorer-ftp-extension" extension will be docum ### Bug fixes +- Fixed ECONNRESET error when trying to upload or create an empty data set member. [#2350](https://github.com/zowe/vscode-extension-for-zowe/issues/2350) + ## `2.10.0` ### New features and enhancements diff --git a/packages/zowe-explorer-ftp-extension/src/ZoweExplorerFtpMvsApi.ts b/packages/zowe-explorer-ftp-extension/src/ZoweExplorerFtpMvsApi.ts index 8da98ef634..cad64ce721 100644 --- a/packages/zowe-explorer-ftp-extension/src/ZoweExplorerFtpMvsApi.ts +++ b/packages/zowe-explorer-ftp-extension/src/ZoweExplorerFtpMvsApi.ts @@ -112,11 +112,6 @@ export class FtpMvsApi extends AbstractFtpApi implements ZoweExplorerApi.IMvs { } public async putContents(inputFilePath: string, dataSetName: string, options: IUploadOptions): Promise { - const transferOptions = { - transferType: options.binary ? TRANSFER_TYPE_BINARY : TRANSFER_TYPE_ASCII, - localFile: inputFilePath, - encoding: options.encoding, - }; const file = path.basename(inputFilePath).replace(/[^a-z0-9]+/gi, ""); const member = file.substr(0, MAX_MEMBER_NAME_LEN); let targetDataset: string; @@ -153,6 +148,16 @@ export class FtpMvsApi extends AbstractFtpApi implements ZoweExplorerApi.IMvs { } const lrecl: number = dsAtrribute.apiResponse.items[0].lrecl; const data = fs.readFileSync(inputFilePath, { encoding: "utf8" }); + const transferOptions = { + transferType: options.binary ? TRANSFER_TYPE_BINARY : TRANSFER_TYPE_ASCII, + localFile: inputFilePath, + encoding: options.encoding, + }; + if (data == "") { + // substitute single space for empty DS contents when saving (prevents FTP error) + transferOptions["content"] = " "; + delete transferOptions["localFile"]; + } const lines = data.split(/\r?\n/); const foundIndex = lines.findIndex((line) => line.length > lrecl); if (foundIndex !== -1) { @@ -243,8 +248,9 @@ export class FtpMvsApi extends AbstractFtpApi implements ZoweExplorerApi.IMvs { public async createDataSetMember(dataSetName: string, options?: IUploadOptions): Promise { const transferOptions = { - transferType: options ? TRANSFER_TYPE_BINARY : TRANSFER_TYPE_ASCII, - content: "", + transferType: options.binary ? TRANSFER_TYPE_BINARY : TRANSFER_TYPE_ASCII, + // we have to provide a single space for content, or zos-node-accessor will fail to upload the data set over FTP + content: " ", encoding: options.encoding, }; const result = this.getDefaultResponse(); From 679bfc9098bdffd2b48ef6fd5e254e513a7cab38 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 12 Sep 2023 14:23:10 -0400 Subject: [PATCH 2/7] test(ftp/ds): Add test case for empty contents Signed-off-by: Trae Yelovich --- .../Mvs/ZoweExplorerFtpMvsApi.unit.test.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/zowe-explorer-ftp-extension/__tests__/__unit__/Mvs/ZoweExplorerFtpMvsApi.unit.test.ts b/packages/zowe-explorer-ftp-extension/__tests__/__unit__/Mvs/ZoweExplorerFtpMvsApi.unit.test.ts index 42aee4a69f..2340ed9e71 100644 --- a/packages/zowe-explorer-ftp-extension/__tests__/__unit__/Mvs/ZoweExplorerFtpMvsApi.unit.test.ts +++ b/packages/zowe-explorer-ftp-extension/__tests__/__unit__/Mvs/ZoweExplorerFtpMvsApi.unit.test.ts @@ -123,6 +123,32 @@ describe("FtpMvsApi", () => { expect(MvsApi.releaseConnection).toBeCalled(); }); + it("should upload empty contents to dataset.", async () => { + const localFile = tmp.tmpNameSync({ tmpdir: "/tmp" }); + + fs.writeFileSync(localFile, ""); + const response = TestUtils.getSingleLineStream(); + DataSetUtils.listDataSets = jest.fn().mockReturnValue([{ dsname: "IBMUSER.DS2", dsorg: "PS", lrecl: 2 }]); + DataSetUtils.uploadDataSet = jest.fn().mockReturnValue(response); + jest.spyOn(MvsApi, "getContents").mockResolvedValue({ apiResponse: { etag: "123" } } as any); + + const mockParams = { + inputFilePath: localFile, + dataSetName: " (IBMUSER).DS2", + options: { encoding: "", returnEtag: true, etag: "utf8" }, + }; + + jest.spyOn(MvsApi as any, "getContentsTag").mockReturnValue(undefined); + jest.spyOn(fs, "readFileSync").mockReturnValue(""); + await MvsApi.putContents(mockParams.inputFilePath, mockParams.dataSetName, mockParams.options); + expect(DataSetUtils.uploadDataSet).toHaveBeenCalledWith({ host: "", password: "", port: "", user: "" }, " (IBMUSER).DS2", { + content: " ", + encoding: "", + transferType: "ascii", + }); + expect(MvsApi.releaseConnection).toBeCalled(); + }); + it("should create dataset.", async () => { DataSetUtils.allocateDataSet = jest.fn(); const DATA_SET_SEQUENTIAL = 4; From e6fb21e8af7f3eefadf31cc8e537fb6539104f3e Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Wed, 13 Sep 2023 10:20:49 -0400 Subject: [PATCH 3/7] test(ftp): update test to ensure localFile isn't in object Signed-off-by: Trae Yelovich --- .../__unit__/Mvs/ZoweExplorerFtpMvsApi.unit.test.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/zowe-explorer-ftp-extension/__tests__/__unit__/Mvs/ZoweExplorerFtpMvsApi.unit.test.ts b/packages/zowe-explorer-ftp-extension/__tests__/__unit__/Mvs/ZoweExplorerFtpMvsApi.unit.test.ts index 2340ed9e71..ac8fcebdbb 100644 --- a/packages/zowe-explorer-ftp-extension/__tests__/__unit__/Mvs/ZoweExplorerFtpMvsApi.unit.test.ts +++ b/packages/zowe-explorer-ftp-extension/__tests__/__unit__/Mvs/ZoweExplorerFtpMvsApi.unit.test.ts @@ -128,24 +128,27 @@ describe("FtpMvsApi", () => { fs.writeFileSync(localFile, ""); const response = TestUtils.getSingleLineStream(); - DataSetUtils.listDataSets = jest.fn().mockReturnValue([{ dsname: "IBMUSER.DS2", dsorg: "PS", lrecl: 2 }]); - DataSetUtils.uploadDataSet = jest.fn().mockReturnValue(response); + DataSetUtils.listDataSets = jest.fn().mockReturnValue([{ dsname: "USER.EMPTYDS", dsorg: "PS", lrecl: 2 }]); + const uploadDataSetMock = jest.fn().mockReturnValue(response); + DataSetUtils.uploadDataSet = uploadDataSetMock; jest.spyOn(MvsApi, "getContents").mockResolvedValue({ apiResponse: { etag: "123" } } as any); const mockParams = { inputFilePath: localFile, - dataSetName: " (IBMUSER).DS2", + dataSetName: "USER.EMPTYDS", options: { encoding: "", returnEtag: true, etag: "utf8" }, }; jest.spyOn(MvsApi as any, "getContentsTag").mockReturnValue(undefined); jest.spyOn(fs, "readFileSync").mockReturnValue(""); await MvsApi.putContents(mockParams.inputFilePath, mockParams.dataSetName, mockParams.options); - expect(DataSetUtils.uploadDataSet).toHaveBeenCalledWith({ host: "", password: "", port: "", user: "" }, " (IBMUSER).DS2", { + expect(DataSetUtils.uploadDataSet).toHaveBeenCalledWith({ host: "", password: "", port: "", user: "" }, "USER.EMPTYDS", { content: " ", encoding: "", transferType: "ascii", }); + // ensure options object at runtime does not have localFile + expect(Object.keys(uploadDataSetMock.mock.calls[0][2]).find((k) => k === "localFile")).toBe(undefined); expect(MvsApi.releaseConnection).toBeCalled(); }); From 72a9770488a55862ae677705cc656d5de34e049e Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 19 Sep 2023 10:23:07 -0400 Subject: [PATCH 4/7] fix(ftp): only use space fix if FTPS is enabled for profile Signed-off-by: Trae Yelovich --- .../src/ZoweExplorerFtpMvsApi.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/zowe-explorer-ftp-extension/src/ZoweExplorerFtpMvsApi.ts b/packages/zowe-explorer-ftp-extension/src/ZoweExplorerFtpMvsApi.ts index cad64ce721..59da88dbf3 100644 --- a/packages/zowe-explorer-ftp-extension/src/ZoweExplorerFtpMvsApi.ts +++ b/packages/zowe-explorer-ftp-extension/src/ZoweExplorerFtpMvsApi.ts @@ -130,9 +130,10 @@ export class FtpMvsApi extends AbstractFtpApi implements ZoweExplorerApi.IMvs { targetDataset = dataSetName + "(" + member + ")"; } const result = this.getDefaultResponse(); + const profile = this.checkedProfile(); let connection; try { - connection = await this.ftpClient(this.checkedProfile()); + connection = await this.ftpClient(profile); if (!connection) { ZoweLogger.logImperativeMessage(result.commandResponse, MessageSeverity.ERROR); throw new Error(result.commandResponse); @@ -153,8 +154,8 @@ export class FtpMvsApi extends AbstractFtpApi implements ZoweExplorerApi.IMvs { localFile: inputFilePath, encoding: options.encoding, }; - if (data == "") { - // substitute single space for empty DS contents when saving (prevents FTP error) + if (profile.profile["secureFtp"] && data == "") { + // substitute single space for empty DS contents when saving (avoids FTPS error) transferOptions["content"] = " "; delete transferOptions["localFile"]; } @@ -247,16 +248,17 @@ export class FtpMvsApi extends AbstractFtpApi implements ZoweExplorerApi.IMvs { } public async createDataSetMember(dataSetName: string, options?: IUploadOptions): Promise { + const profile = this.checkedProfile(); const transferOptions = { transferType: options.binary ? TRANSFER_TYPE_BINARY : TRANSFER_TYPE_ASCII, - // we have to provide a single space for content, or zos-node-accessor will fail to upload the data set over FTP - content: " ", + // we have to provide a single space for content over FTPS, or it will fail to upload + content: profile.profile["secureFtp"] ? " " : "", encoding: options.encoding, }; const result = this.getDefaultResponse(); let connection; try { - connection = await this.ftpClient(this.checkedProfile()); + connection = await this.ftpClient(profile); if (!connection) { throw new Error(result.commandResponse); } From 5f51da0f99dfcd74ef96715871d9d6cebfafe4e1 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 19 Sep 2023 10:54:21 -0400 Subject: [PATCH 5/7] test(ftp): make test case more specific for FTPS space fix Signed-off-by: Trae Yelovich --- .../__unit__/Mvs/ZoweExplorerFtpMvsApi.unit.test.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/zowe-explorer-ftp-extension/__tests__/__unit__/Mvs/ZoweExplorerFtpMvsApi.unit.test.ts b/packages/zowe-explorer-ftp-extension/__tests__/__unit__/Mvs/ZoweExplorerFtpMvsApi.unit.test.ts index ac8fcebdbb..1836670676 100644 --- a/packages/zowe-explorer-ftp-extension/__tests__/__unit__/Mvs/ZoweExplorerFtpMvsApi.unit.test.ts +++ b/packages/zowe-explorer-ftp-extension/__tests__/__unit__/Mvs/ZoweExplorerFtpMvsApi.unit.test.ts @@ -36,7 +36,7 @@ const MvsApi = new FtpMvsApi(); describe("FtpMvsApi", () => { beforeEach(() => { - MvsApi.checkedProfile = jest.fn().mockReturnValue({ message: "success", type: "zftp", failNotFound: false }); + MvsApi.checkedProfile = jest.fn().mockReturnValue({ message: "success", type: "zftp", profile: { secureFtp: false }, failNotFound: false }); MvsApi.ftpClient = jest.fn().mockReturnValue({ host: "", user: "", password: "", port: "" }); MvsApi.releaseConnection = jest.fn(); sessionMap.get = jest.fn().mockReturnValue({ mvsListConnection: { connected: true } }); @@ -123,7 +123,7 @@ describe("FtpMvsApi", () => { expect(MvsApi.releaseConnection).toBeCalled(); }); - it("should upload empty contents to dataset.", async () => { + it("should upload single space to dataset when secureFtp is true and contents are empty", async () => { const localFile = tmp.tmpNameSync({ tmpdir: "/tmp" }); fs.writeFileSync(localFile, ""); @@ -138,6 +138,14 @@ describe("FtpMvsApi", () => { dataSetName: "USER.EMPTYDS", options: { encoding: "", returnEtag: true, etag: "utf8" }, }; + jest.spyOn(MvsApi, "checkedProfile").mockReturnValueOnce({ + type: "zftp", + message: "", + profile: { + secureFtp: true, + }, + failNotFound: false, + }); jest.spyOn(MvsApi as any, "getContentsTag").mockReturnValue(undefined); jest.spyOn(fs, "readFileSync").mockReturnValue(""); From f42c442204aee8e959b3b7fc890dde7c0e5cf0d5 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 19 Sep 2023 14:16:14 -0400 Subject: [PATCH 6/7] fix(ftp): access/modify object properties directly Signed-off-by: Trae Yelovich --- .../src/ZoweExplorerFtpMvsApi.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/zowe-explorer-ftp-extension/src/ZoweExplorerFtpMvsApi.ts b/packages/zowe-explorer-ftp-extension/src/ZoweExplorerFtpMvsApi.ts index 59da88dbf3..75bd6bfaac 100644 --- a/packages/zowe-explorer-ftp-extension/src/ZoweExplorerFtpMvsApi.ts +++ b/packages/zowe-explorer-ftp-extension/src/ZoweExplorerFtpMvsApi.ts @@ -149,15 +149,15 @@ export class FtpMvsApi extends AbstractFtpApi implements ZoweExplorerApi.IMvs { } const lrecl: number = dsAtrribute.apiResponse.items[0].lrecl; const data = fs.readFileSync(inputFilePath, { encoding: "utf8" }); - const transferOptions = { + const transferOptions: Record = { transferType: options.binary ? TRANSFER_TYPE_BINARY : TRANSFER_TYPE_ASCII, localFile: inputFilePath, encoding: options.encoding, }; - if (profile.profile["secureFtp"] && data == "") { + if (profile.profile.secureFtp && data == "") { // substitute single space for empty DS contents when saving (avoids FTPS error) - transferOptions["content"] = " "; - delete transferOptions["localFile"]; + transferOptions.content = " "; + delete transferOptions.localFile; } const lines = data.split(/\r?\n/); const foundIndex = lines.findIndex((line) => line.length > lrecl); @@ -252,7 +252,7 @@ export class FtpMvsApi extends AbstractFtpApi implements ZoweExplorerApi.IMvs { const transferOptions = { transferType: options.binary ? TRANSFER_TYPE_BINARY : TRANSFER_TYPE_ASCII, // we have to provide a single space for content over FTPS, or it will fail to upload - content: profile.profile["secureFtp"] ? " " : "", + content: profile.profile.secureFtp ? " " : "", encoding: options.encoding, }; const result = this.getDefaultResponse(); From 3e24aab4b8603386871ecf3c9232bcbf0051acd8 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 19 Sep 2023 14:22:06 -0400 Subject: [PATCH 7/7] fix(ftp): use eqeqeq for data in putContents Signed-off-by: Trae Yelovich --- .../zowe-explorer-ftp-extension/src/ZoweExplorerFtpMvsApi.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/zowe-explorer-ftp-extension/src/ZoweExplorerFtpMvsApi.ts b/packages/zowe-explorer-ftp-extension/src/ZoweExplorerFtpMvsApi.ts index 75bd6bfaac..4ad04c06bb 100644 --- a/packages/zowe-explorer-ftp-extension/src/ZoweExplorerFtpMvsApi.ts +++ b/packages/zowe-explorer-ftp-extension/src/ZoweExplorerFtpMvsApi.ts @@ -154,7 +154,7 @@ export class FtpMvsApi extends AbstractFtpApi implements ZoweExplorerApi.IMvs { localFile: inputFilePath, encoding: options.encoding, }; - if (profile.profile.secureFtp && data == "") { + if (profile.profile.secureFtp && data === "") { // substitute single space for empty DS contents when saving (avoids FTPS error) transferOptions.content = " "; delete transferOptions.localFile;