Skip to content

Commit cafc6e6

Browse files
authored
Merge pull request #2703 from codefori/fix/copyMoveKeepCCSID
Run `cp` and `mv` in QSH to keep CCSID attribute
2 parents 01dece5 + 6077f1e commit cafc6e6

File tree

3 files changed

+73
-10
lines changed

3 files changed

+73
-10
lines changed

src/api/IBMiContent.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ export default class IBMiContent {
103103
* @param content Raw content
104104
* @param encoding Optional encoding to write.
105105
*/
106-
async writeStreamfileRaw(originalPath: string, content: string|Uint8Array, encoding?: string) {
106+
async writeStreamfileRaw(originalPath: string, content: string | Uint8Array, encoding?: string) {
107107
const client = this.ibmi.client!;
108108
const features = this.ibmi.remoteFeatures;
109109
const tmpobj = await tmpFile();
@@ -228,7 +228,7 @@ export default class IBMiContent {
228228
* @param content
229229
*/
230230
async uploadMemberContent(library: string, sourceFile: string, member: string, content: string | Uint8Array): Promise<boolean>;
231-
231+
232232
/**
233233
* @deprecated Will be removed in `v3.0.0`; use {@link IBMiContent.uploadMemberContent()} without the `asp` parameter instead.
234234
* @param asp
@@ -315,7 +315,7 @@ export default class IBMiContent {
315315
*/
316316
runSQL(statements: string) {
317317
console.warn(`[Code for IBM i] runSQL is deprecated and will be removed by 4.0.0. Use IBMi.runSQL instead.`);
318-
318+
319319
return this.ibmi.runSQL(statements);
320320
}
321321

@@ -1149,4 +1149,26 @@ export default class IBMiContent {
11491149
downloadDirectory(localDirectory: EditorPath, remoteDirectory: string, options?: node_ssh.SSHGetPutDirectoryOptions) {
11501150
return this.ibmi.client!.getDirectory(Tools.fileToPath(localDirectory), remoteDirectory, options);
11511151
}
1152+
1153+
/**
1154+
* Copy one or more folders or files into a directory. Uses QSH's `cp` to keep all the attributes of the original file into its copy.
1155+
* @param paths one or more files/folders to copy
1156+
* @param toDirectory the directory where the files/folders will be copied into
1157+
* @returns the {@link CommandResult} of the `cp` command execution
1158+
*/
1159+
async copy(paths: string | string[], toDirectory: string) {
1160+
paths = Array.isArray(paths) ? paths : [paths];
1161+
return this.ibmi.runCommand({ command: `cp -r ${paths.map(path => Tools.escapePath(path)).join(" ")} ${Tools.escapePath(toDirectory)}`, environment: "qsh" });
1162+
}
1163+
1164+
/**
1165+
* Move one or more folders or files into a directory. Uses QSH's `mv` to ensures attributes are not altered during the opration.
1166+
* @param paths one or more files/folders to copy
1167+
* @param toDirectory the directory where the files/folders will be copied into
1168+
* @returns the {@link CommandResult} of the `mv` command execution
1169+
*/
1170+
async move(paths: string | string[], toDirectory: string) {
1171+
paths = Array.isArray(paths) ? paths : [paths];
1172+
return await this.ibmi.runCommand({ command: `mv ${paths.map(path => Tools.escapePath(path)).join(" ")} ${Tools.escapePath(toDirectory)}`, environment: "qsh" });
1173+
}
11521174
}

src/api/tests/suites/content.test.ts

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ import util from 'util';
55
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
66
import IBMi from '../../IBMi';
77
import { Tools } from '../../Tools';
8-
import { CONNECTION_TIMEOUT, disposeConnection, newConnection } from '../connection';
98
import { ModuleExport, ProgramExportImportInfo } from '../../types';
9+
import { CONNECTION_TIMEOUT, disposeConnection, newConnection } from '../connection';
1010

11-
describe('Content Tests', {concurrent: true}, () => {
11+
describe('Content Tests', { concurrent: true }, () => {
1212
let connection: IBMi
1313
beforeAll(async () => {
1414
connection = await newConnection();
@@ -521,7 +521,7 @@ describe('Content Tests', {concurrent: true}, () => {
521521
expect(error).toBeInstanceOf(Tools.SqlError);
522522
expect(error.sqlstate).toBe('38501');
523523
}
524-
}, {timeout: 25000});
524+
}, { timeout: 25000 });
525525

526526
it('Test @clCommand + select statement', async () => {
527527
const content = connection.getContent();
@@ -717,4 +717,40 @@ describe('Content Tests', {concurrent: true}, () => {
717717
}
718718
});
719719
});
720+
721+
it('Copy and move streamfiles', async () => {
722+
const content = connection.getContent();
723+
await connection.withTempDirectory(async directory => {
724+
const checkFile = async (path: string, ccsid: number) => {
725+
expect(await content.testStreamFile(path, "w")).toBe(true);
726+
const attributes = await content.getAttributes(path, "CCSID");
727+
expect(attributes).toBeDefined();
728+
expect(attributes!["CCSID"]).toBe(String(ccsid));
729+
}
730+
731+
const unicodeFile = "unicode";
732+
const ccsid37File = "ccsid37";
733+
734+
await content.createStreamFile(`${directory}/${unicodeFile}`);
735+
await checkFile(`${directory}/${unicodeFile}`, 1208);
736+
await content.createStreamFile(`${directory}/${ccsid37File}`);
737+
await connection.sendCommand({ command: `${connection.remoteFeatures.attr} ${directory}/${ccsid37File} CCSID=37` });
738+
await checkFile(`${directory}/${ccsid37File}`, 37);
739+
const files = [`${directory}/${unicodeFile}`, `${directory}/${ccsid37File}`];
740+
741+
expect((await connection.sendCommand({ command: `mkdir ${directory}/copy` })).code).toBe(0);
742+
expect((await content.copy(files, `${directory}/copy`)).code).toBe(0);
743+
expect(await content.testStreamFile(`${directory}/${unicodeFile}`, "f")).toBe(true);
744+
expect(await content.testStreamFile(`${directory}/${ccsid37File}`, "f")).toBe(true);
745+
await checkFile(`${directory}/copy/${unicodeFile}`, 1208);
746+
await checkFile(`${directory}/copy/${ccsid37File}`, 37);
747+
748+
expect((await connection.sendCommand({ command: `mkdir ${directory}/move` })).code).toBe(0);
749+
expect((await content.move(files, `${directory}/move`)).code).toBe(0);
750+
expect(await content.testStreamFile(`${directory}/${unicodeFile}`, "f")).toBe(false);
751+
expect(await content.testStreamFile(`${directory}/${ccsid37File}`, "f")).toBe(false);
752+
await checkFile(`${directory}/move/${unicodeFile}`, 1208);
753+
await checkFile(`${directory}/move/${ccsid37File}`, 37);
754+
});
755+
});
720756
});

src/ui/views/ifsBrowser.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -253,13 +253,15 @@ class IFSBrowserDragAndDrop implements vscode.TreeDragAndDropController<IFSItem>
253253

254254
if (action) {
255255
let result;
256+
const froms = ifsBrowserItems.map(item => item.path);
257+
const to = toDirectory.path;
256258
switch (action) {
257259
case "copy":
258-
result = await connection.sendCommand({ command: `cp -r ${ifsBrowserItems.map(item => Tools.escapePath(item.path)).join(" ")} ${Tools.escapePath(toDirectory.path)}` });
260+
result = await connection.getContent().copy(froms, to);
259261
break;
260262

261263
case "move":
262-
result = await connection.sendCommand({ command: `mv ${ifsBrowserItems.map(item => Tools.escapePath(item.path)).join(" ")} ${Tools.escapePath(toDirectory.path)}` });
264+
result = await await connection.getContent().move(froms, to);
263265
ifsBrowserItems.map(item => item.parent)
264266
.filter(Tools.distinct)
265267
.forEach(folder => folder?.refresh?.());
@@ -654,7 +656,7 @@ Please type "{0}" to confirm deletion.`, dirName);
654656
if (target) {
655657
const targetPath = path.posix.isAbsolute(target) ? target : path.posix.join(homeDirectory, target);
656658
try {
657-
const moveResult = await connection.sendCommand({ command: `mv ${Tools.escapePath(node.path)} ${Tools.escapePath(targetPath)}` });
659+
const moveResult = await connection.runCommand({ command: `mv ${Tools.escapePath(node.path)} ${Tools.escapePath(targetPath)}`, environment: "qsh" });
658660
if (moveResult.code !== 0) {
659661
throw moveResult.stderr;
660662
}
@@ -704,7 +706,10 @@ Please type "{0}" to confirm deletion.`, dirName);
704706
if (target) {
705707
const targetPath = target.startsWith(`/`) ? target : homeDirectory + `/` + target;
706708
try {
707-
await connection.sendCommand({ command: `cp -r ${Tools.escapePath(node.path)} ${Tools.escapePath(targetPath)}` });
709+
const result = await connection.getContent().copy(node.path, targetPath);
710+
if (result.code !== 0) {
711+
throw result.stderr;
712+
}
708713
if (IBMi.connectionManager.get(`autoRefresh`)) {
709714
ifsBrowser.refresh();
710715
}

0 commit comments

Comments
 (0)