diff --git a/packages/zowe-explorer-ftp-extension/CHANGELOG.md b/packages/zowe-explorer-ftp-extension/CHANGELOG.md index 955460b2a1..8e35232235 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.11.0` ### Bug fixes 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..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,6 +123,43 @@ describe("FtpMvsApi", () => { expect(MvsApi.releaseConnection).toBeCalled(); }); + 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, ""); + const response = TestUtils.getSingleLineStream(); + 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: "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(""); + await MvsApi.putContents(mockParams.inputFilePath, mockParams.dataSetName, mockParams.options); + 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(); + }); + it("should create dataset.", async () => { DataSetUtils.allocateDataSet = jest.fn(); const DATA_SET_SEQUENTIAL = 4; diff --git a/packages/zowe-explorer-ftp-extension/src/ZoweExplorerFtpMvsApi.ts b/packages/zowe-explorer-ftp-extension/src/ZoweExplorerFtpMvsApi.ts index 8da98ef634..4ad04c06bb 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; @@ -135,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,6 +149,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: Record = { + transferType: options.binary ? TRANSFER_TYPE_BINARY : TRANSFER_TYPE_ASCII, + localFile: inputFilePath, + encoding: options.encoding, + }; + if (profile.profile.secureFtp && data === "") { + // substitute single space for empty DS contents when saving (avoids FTPS error) + transferOptions.content = " "; + delete transferOptions.localFile; + } const lines = data.split(/\r?\n/); const foundIndex = lines.findIndex((line) => line.length > lrecl); if (foundIndex !== -1) { @@ -242,15 +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 ? 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 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); }