Skip to content

Commit 0fba305

Browse files
Fix import of binary files such as webapp images (intersystems-community#1060) (intersystems-community#1064)
1 parent 603e7a4 commit 0fba305

File tree

7 files changed

+212
-51
lines changed

7 files changed

+212
-51
lines changed

package-lock.json

Lines changed: 78 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1589,6 +1589,7 @@
15891589
},
15901590
"devDependencies": {
15911591
"@types/glob": "^7.1.2",
1592+
"@types/istextorbinary": "2.3.1",
15921593
"@types/mocha": "^7.0.2",
15931594
"@types/node": "^14.18.0",
15941595
"@types/vscode": "^1.66.0",
@@ -1622,6 +1623,7 @@
16221623
"domexception": "^2.0.1",
16231624
"glob": "^7.1.6",
16241625
"iconv-lite": "^0.6.0",
1626+
"istextorbinary": "^6.0.0",
16251627
"mkdirp": "^1.0.4",
16261628
"node-cmd": "^5.0.0",
16271629
"node-fetch-cjs": "3.1.1",

src/commands/compile.ts

Lines changed: 79 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@ import {
1414
import { DocumentContentProvider } from "../providers/DocumentContentProvider";
1515
import {
1616
cspAppsForUri,
17+
CurrentBinaryFile,
1718
currentFile,
1819
CurrentFile,
1920
currentFileFromContent,
21+
CurrentTextFile,
2022
isClassDeployed,
2123
notNull,
2224
outputChannel,
@@ -25,6 +27,7 @@ import {
2527
import { PackageNode } from "../explorer/models/packageNode";
2628
import { NodeBase } from "../explorer/models/nodeBase";
2729
import { RootNode } from "../explorer/models/rootNode";
30+
import { isText } from "istextorbinary";
2831

2932
async function compileFlags(): Promise<string> {
3033
const defaultFlags = config().compileFlags;
@@ -46,7 +49,7 @@ async function compileFlags(): Promise<string> {
4649
* @param force If passed true, use server mtime.
4750
* @return mtime timestamp or -1.
4851
*/
49-
export async function checkChangedOnServer(file: CurrentFile, force = false): Promise<number> {
52+
export async function checkChangedOnServer(file: CurrentTextFile | CurrentBinaryFile, force = false): Promise<number> {
5053
if (!file || !file.uri) {
5154
return -1;
5255
}
@@ -57,11 +60,16 @@ export async function checkChangedOnServer(file: CurrentFile, force = false): Pr
5760
.getDoc(file.name)
5861
.then((data) => data.result)
5962
.then(async ({ ts, content }) => {
60-
const fileContent = file.content.split(/\r?\n/);
6163
const serverTime = Number(new Date(ts + "Z"));
62-
const sameContent = force
63-
? false
64-
: content.every((line, index) => line.trim() == (fileContent[index] || "").trim());
64+
let sameContent: boolean;
65+
if (typeof file.content === "string") {
66+
const fileContent = file.content.split(/\r?\n/);
67+
sameContent = force
68+
? false
69+
: (content as string[]).every((line, index) => line.trim() == (fileContent[index] || "").trim());
70+
} else {
71+
sameContent = force ? false : Buffer.compare(content as Buffer, file.content) === 0;
72+
}
6573
const mtime =
6674
force || sameContent ? serverTime : Math.max((await vscode.workspace.fs.stat(file.uri)).mtime, serverTime);
6775
return mtime;
@@ -71,23 +79,43 @@ export async function checkChangedOnServer(file: CurrentFile, force = false): Pr
7179
return mtime;
7280
}
7381

74-
async function importFile(file: CurrentFile, ignoreConflict?: boolean, skipDeplCheck = false): Promise<any> {
82+
async function importFile(
83+
file: CurrentTextFile | CurrentBinaryFile,
84+
ignoreConflict?: boolean,
85+
skipDeplCheck = false
86+
): Promise<any> {
7587
const api = new AtelierAPI(file.uri);
7688
if (file.name.split(".").pop().toLowerCase() === "cls" && !skipDeplCheck) {
7789
if (await isClassDeployed(file.name, api)) {
7890
vscode.window.showErrorMessage(`Cannot import ${file.name} because it is deployed on the server.`, "Dismiss");
7991
return Promise.reject();
8092
}
8193
}
82-
const content = file.content.split(/\r?\n/);
94+
let enc: boolean;
95+
let content: string[];
96+
if (typeof file.content === "string") {
97+
enc = false;
98+
content = file.content.split(/\r?\n/);
99+
} else {
100+
// Base64 encoding must be in chunk size multiple of 3 and within the server's potential 32K string limit
101+
// Output is 4 chars for each 3 input, so 24573/3*4 = 32764
102+
const chunkSize = 24573;
103+
let start = 0;
104+
content = [];
105+
enc = true;
106+
while (start < file.content.byteLength) {
107+
content.push(file.content.toString("base64", start, start + chunkSize));
108+
start += chunkSize;
109+
}
110+
}
83111
const mtime = await checkChangedOnServer(file);
84112
ignoreConflict = ignoreConflict || mtime < 0 || (file.uri.scheme === "file" && config("overwriteServerChanges"));
85113
return api
86114
.putDoc(
87115
file.name,
88116
{
89117
content,
90-
enc: false,
118+
enc,
91119
mtime,
92120
},
93121
ignoreConflict
@@ -112,14 +140,16 @@ async function importFile(file: CurrentFile, ignoreConflict?: boolean, skipDeplC
112140
})
113141
.catch((error) => {
114142
if (error?.statusCode == 409) {
143+
const choices: string[] = [];
144+
if (!enc) {
145+
choices.push("Compare");
146+
}
147+
choices.push("Overwrite on Server", "Pull Server Changes", "Cancel");
115148
return vscode.window
116149
.showErrorMessage(
117150
`Failed to import '${file.name}': The version of the file on the server is newer.
118151
What do you want to do?`,
119-
"Compare",
120-
"Overwrite on Server",
121-
"Pull Server Changes",
122-
"Cancel"
152+
...choices
123153
)
124154
.then((action) => {
125155
switch (action) {
@@ -200,7 +230,7 @@ function updateOthers(others: string[], baseUri: vscode.Uri) {
200230
});
201231
}
202232

203-
export async function loadChanges(files: CurrentFile[]): Promise<any> {
233+
export async function loadChanges(files: (CurrentTextFile | CurrentBinaryFile)[]): Promise<any> {
204234
if (!files.length) {
205235
return;
206236
}
@@ -210,11 +240,19 @@ export async function loadChanges(files: CurrentFile[]): Promise<any> {
210240
api
211241
.getDoc(file.name)
212242
.then(async (data) => {
213-
const content = (data.result.content || []).join(file.eol === vscode.EndOfLine.LF ? "\n" : "\r\n");
214243
const mtime = Number(new Date(data.result.ts + "Z"));
215244
workspaceState.update(`${file.uniqueId}:mtime`, mtime > 0 ? mtime : undefined);
216245
if (file.uri.scheme === "file") {
217-
await vscode.workspace.fs.writeFile(file.uri, new TextEncoder().encode(content));
246+
if (Buffer.isBuffer(data.result.content)) {
247+
// This is a binary file
248+
await vscode.workspace.fs.writeFile(file.uri, data.result.content);
249+
} else {
250+
// This is a text file
251+
const content = (data.result.content || []).join(
252+
(file as CurrentTextFile).eol === vscode.EndOfLine.LF ? "\n" : "\r\n"
253+
);
254+
await vscode.workspace.fs.writeFile(file.uri, new TextEncoder().encode(content));
255+
}
218256
} else if (file.uri.scheme === FILESYSTEM_SCHEMA || file.uri.scheme === FILESYSTEM_READONLY_SCHEMA) {
219257
fileSystemProvider.fireFileChanged(file.uri);
220258
}
@@ -410,23 +448,37 @@ export async function namespaceCompile(askFlags = false): Promise<any> {
410448
);
411449
}
412450

413-
function importFiles(files: string[], noCompile = false) {
414-
return Promise.all<CurrentFile>(
451+
async function importFiles(files: string[], noCompile = false) {
452+
const toCompile: CurrentFile[] = [];
453+
await Promise.all<void>(
415454
files.map(
416-
throttleRequests((file: string) =>
417-
vscode.workspace.fs
418-
.readFile(vscode.Uri.file(file))
419-
.then((contentBytes) => new TextDecoder().decode(contentBytes))
420-
.then((content) => currentFileFromContent(vscode.Uri.file(file), content))
455+
throttleRequests((file: string) => {
456+
const uri = vscode.Uri.file(file);
457+
return vscode.workspace.fs
458+
.readFile(uri)
459+
.then((contentBytes) => {
460+
if (isText(file, Buffer.from(contentBytes))) {
461+
const textFile = currentFileFromContent(uri, new TextDecoder().decode(contentBytes));
462+
toCompile.push(textFile);
463+
return textFile;
464+
} else {
465+
return currentFileFromContent(uri, Buffer.from(contentBytes));
466+
}
467+
})
421468
.then((curFile) =>
422469
importFile(curFile).then((data) => {
423470
outputChannel.appendLine("Imported file: " + curFile.fileName);
424-
return curFile;
471+
return;
425472
})
426-
)
427-
)
473+
);
474+
})
428475
)
429-
).then(noCompile ? Promise.resolve : compile);
476+
);
477+
478+
if (!noCompile && toCompile.length > 0) {
479+
return compile(toCompile);
480+
}
481+
return;
430482
}
431483

432484
export async function importFolder(uri: vscode.Uri, noCompile = false): Promise<any> {
@@ -437,7 +489,7 @@ export async function importFolder(uri: vscode.Uri, noCompile = false): Promise<
437489
let globpattern = "*.{cls,inc,int,mac}";
438490
if (cspAppsForUri(uri).findIndex((cspApp) => uri.path.includes(cspApp + "/") || uri.path.endsWith(cspApp)) != -1) {
439491
// This folder is a CSP application, so import all files
440-
// We need to include eveything becuase CSP applications can
492+
// We need to include eveything because CSP applications can
441493
// include non-InterSystems files
442494
globpattern = "*";
443495
}

0 commit comments

Comments
 (0)