Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(ftp/ds): ECONNRESET FTPS error for empty dataset contents #2460

Merged
merged 11 commits into from
Sep 20, 2023
2 changes: 2 additions & 0 deletions packages/zowe-explorer-ftp-extension/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 } });
Expand Down Expand Up @@ -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;
Expand Down
26 changes: 17 additions & 9 deletions packages/zowe-explorer-ftp-extension/src/ZoweExplorerFtpMvsApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,11 +112,6 @@ export class FtpMvsApi extends AbstractFtpApi implements ZoweExplorerApi.IMvs {
}

public async putContents(inputFilePath: string, dataSetName: string, options: IUploadOptions): Promise<zowe.IZosFilesResponse> {
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;
Expand All @@ -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);
Expand All @@ -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<string, any> = {
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) {
Expand Down Expand Up @@ -242,15 +248,17 @@ export class FtpMvsApi extends AbstractFtpApi implements ZoweExplorerApi.IMvs {
}

public async createDataSetMember(dataSetName: string, options?: IUploadOptions): Promise<zowe.IZosFilesResponse> {
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);
}
Expand Down
Loading