Skip to content

Commit b5ba27c

Browse files
feat(performance): removed double memory usage when reading/writing text files (Acode-Foundation#2359)
* feat: removed double memory usage when reading/writing text files * feat: improvements * format * fix: regression * format * feat: pin utf-8 for json * format
1 parent a6f53da commit b5ba27c

4 files changed

Lines changed: 153 additions & 19 deletions

File tree

src/fileSystem/externalFs.js

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import loader from "dialogs/loader";
2-
import { decode, encode } from "utils/encodings";
2+
import { decode, encode, getEncodingName } from "utils/encodings";
33
import helpers from "utils/helpers";
44
import Url from "utils/Url";
55

@@ -11,12 +11,26 @@ const externalFs = {
1111
});
1212
},
1313

14+
async readAsText(url, encoding) {
15+
url = await this.formatUri(url);
16+
return new Promise((resolve, reject) => {
17+
sdcard.readAsText(url, encoding, resolve, reject);
18+
});
19+
},
20+
1421
async writeFile(filename, data) {
1522
return new Promise(async (resolve, reject) => {
1623
sdcard.write(filename, data, resolve, reject);
1724
});
1825
},
1926

27+
async writeText(filename, data, encoding) {
28+
filename = await this.formatUri(filename);
29+
return new Promise((resolve, reject) => {
30+
sdcard.writeText(filename, data, encoding, resolve, reject);
31+
});
32+
},
33+
2034
async copy(src, dest) {
2135
return new Promise((resolve, reject) => {
2236
sdcard.copy(src, dest, resolve, reject);
@@ -178,17 +192,23 @@ function createFs(url) {
178192
return externalFs.listDir(url);
179193
},
180194
async readFile(encoding) {
181-
let { data } = await externalFs.readFile(url, encoding);
182-
183195
if (encoding) {
184-
data = await decode(data, encoding);
196+
const isJson = encoding === "json";
197+
const charset = getEncodingName(isJson ? "utf-8" : encoding);
198+
const text = await externalFs.readAsText(url, charset);
199+
const stripped = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
200+
return isJson ? JSON.parse(stripped) : stripped;
185201
}
186202

203+
let { data } = await externalFs.readFile(url);
187204
return data;
188205
},
189206
async writeFile(content, encoding) {
190207
if (typeof content === "string" && encoding) {
191-
content = await encode(content, encoding);
208+
const charset = getEncodingName(
209+
encoding === "json" ? "utf-8" : encoding,
210+
);
211+
return externalFs.writeText(url, content, charset);
192212
}
193213
return externalFs.writeFile(url, content);
194214
},

src/plugins/sdcard/src/android/SDcard.java

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
import java.io.InputStream;
2525
import java.io.OutputStream;
2626
import java.io.PrintWriter;
27+
import java.io.BufferedReader;
28+
import java.io.InputStreamReader;
29+
import java.nio.charset.Charset;
2730
import java.net.URLConnection;
2831
import java.util.ArrayList;
2932
import java.util.Arrays;
@@ -108,6 +111,9 @@ public boolean execute(
108111
case "read":
109112
readFile(arg1, callback);
110113
break;
114+
case "readAsText":
115+
readAsText(arg1, arg2, callback);
116+
break;
111117
case "write":
112118
writeFile(
113119
formatUri(arg1),
@@ -116,6 +122,9 @@ public boolean execute(
116122
callback
117123
);
118124
break;
125+
case "writeText":
126+
writeText(arg1, arg2, arg3, callback);
127+
break;
119128
case "rename":
120129
rename(arg1, arg2, callback);
121130
break;
@@ -476,6 +485,102 @@ public void run() {
476485
);
477486
}
478487

488+
private void readAsText(final String filename, final String encoding, final CallbackContext callback) {
489+
cordova
490+
.getThreadPool()
491+
.execute(
492+
new Runnable() {
493+
public void run() {
494+
try {
495+
String formattedUri = formatUri(filename);
496+
Uri uri = Uri.parse(formattedUri);
497+
498+
String charSetName = encoding;
499+
if (charSetName == null || charSetName.isEmpty() || "auto".equalsIgnoreCase(charSetName)) {
500+
charSetName = "UTF-8";
501+
}
502+
if (!Charset.isSupported(charSetName)) {
503+
callback.error("Charset not supported: " + charSetName);
504+
return;
505+
}
506+
Charset charset = Charset.forName(charSetName);
507+
508+
InputStream is = context
509+
.getContentResolver()
510+
.openInputStream(uri);
511+
512+
if (is == null) {
513+
callback.error("File not found");
514+
return;
515+
}
516+
517+
StringBuilder sb = new StringBuilder();
518+
try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, charset))) {
519+
char[] buffer = new char[8192];
520+
int charsRead;
521+
while ((charsRead = reader.read(buffer)) != -1) {
522+
sb.append(buffer, 0, charsRead);
523+
}
524+
}
525+
callback.success(sb.toString());
526+
} catch (Exception e) {
527+
callback.error(e.toString());
528+
}
529+
}
530+
}
531+
);
532+
}
533+
534+
private void writeText(
535+
final String filename,
536+
final String content,
537+
final String encoding,
538+
final CallbackContext callback
539+
) {
540+
final Context context = this.context;
541+
542+
cordova
543+
.getThreadPool()
544+
.execute(
545+
new Runnable() {
546+
public void run() {
547+
try {
548+
String formattedUri = formatUri(filename);
549+
DocumentFile file = getFile(formattedUri);
550+
if (file == null) {
551+
callback.error("File not found.");
552+
return;
553+
}
554+
if (canWrite(file.getUri())) {
555+
String charSetName = encoding;
556+
if (charSetName == null || charSetName.isEmpty() || "auto".equalsIgnoreCase(charSetName)) {
557+
charSetName = "UTF-8";
558+
}
559+
if (!Charset.isSupported(charSetName)) {
560+
callback.error("Charset not supported: " + charSetName);
561+
return;
562+
}
563+
Charset charset = Charset.forName(charSetName);
564+
565+
try (OutputStream op = context
566+
.getContentResolver()
567+
.openOutputStream(file.getUri(), "rwt")) {
568+
byte[] bytes = content.getBytes(charset);
569+
op.write(bytes);
570+
op.flush();
571+
}
572+
callback.success("OK");
573+
} else {
574+
callback.error("No write permission");
575+
}
576+
} catch (Exception e) {
577+
callback.error(e.toString());
578+
}
579+
}
580+
}
581+
);
582+
}
583+
479584
private void writeFile(
480585
final String filename,
481586
final String content,

src/plugins/sdcard/www/plugin.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,16 @@ module.exports = {
4444
read: function (filename, onSuccess, onFail) {
4545
cordova.exec(onSuccess, onFail, 'SDcard', 'read', [filename]);
4646
},
47+
readAsText: function (filename, encoding, onSuccess, onFail) {
48+
cordova.exec(onSuccess, onFail, 'SDcard', 'readAsText', [filename, encoding]);
49+
},
4750
write: function (filename, content, onSuccess, onFail) {
4851
var _isBuffer = content instanceof ArrayBuffer;
4952
cordova.exec(onSuccess, onFail, 'SDcard', 'write', [filename, content, _isBuffer]);
5053
},
54+
writeText: function (filename, content, encoding, onSuccess, onFail) {
55+
cordova.exec(onSuccess, onFail, 'SDcard', 'writeText', [filename, content, encoding]);
56+
},
5157
stats: function (filename, onSuccess, onFail) {
5258
cordova.exec(onSuccess, onFail, 'SDcard', 'stats', [filename]);
5359
},

src/utils/encodings.js

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,21 @@ export async function detectEncoding(buffer) {
151151
return def === "auto" ? "UTF-8" : def || "UTF-8";
152152
}
153153

154+
/**
155+
* Resolves a charset string to the canonical encoding name.
156+
* @param {string} charset
157+
* @returns {string}
158+
*/
159+
export function getEncodingName(charset) {
160+
if (!charset) {
161+
charset = settings.value.defaultFileEncoding;
162+
}
163+
164+
if (charset === "auto") charset = "UTF-8";
165+
166+
return getEncoding(charset).name;
167+
}
168+
154169
/**
155170
* Decodes arrayBuffer to String according given encoding type
156171
* @param {ArrayBuffer} buffer
@@ -165,13 +180,7 @@ export async function decode(buffer, charset) {
165180
isJson = true;
166181
}
167182

168-
if (!charset) {
169-
charset = settings.value.defaultFileEncoding;
170-
}
171-
172-
if (charset === "auto") charset = "UTF-8";
173-
174-
charset = getEncoding(charset).name;
183+
charset = getEncodingName(charset);
175184
const text = await execDecode(buffer, charset);
176185

177186
if (isJson) {
@@ -188,13 +197,7 @@ export async function decode(buffer, charset) {
188197
* @returns {Promise<ArrayBuffer>}
189198
*/
190199
export function encode(text, charset) {
191-
if (!charset) {
192-
charset = settings.value.defaultFileEncoding;
193-
}
194-
195-
if (charset === "auto") charset = "UTF-8";
196-
197-
charset = getEncoding(charset).name;
200+
charset = getEncodingName(charset);
198201
return execEncode(text, charset);
199202
}
200203

0 commit comments

Comments
 (0)