Skip to content

Commit c63a50a

Browse files
authored
feat(file): copy sync and async support (#10273)
1 parent 1b17e23 commit c63a50a

File tree

9 files changed

+308
-1
lines changed

9 files changed

+308
-1
lines changed

apps/toolbox/src/pages/fs-helper.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,8 @@ export function pickFile() {
109109
const file = args.intent.getData().toString();
110110
//const file = File.fromPath(args.intent.getData().toString());
111111
//console.log(file);
112-
readFile(file);
112+
//readFile(file);
113+
copyFile(file);
113114
}
114115
});
115116
const Intent = android.content.Intent;
@@ -172,6 +173,41 @@ function saveFile(selected, readSync) {
172173
console.log('==== SAVE END =========');
173174
}
174175

176+
function copyFile(file) {
177+
const picked = File.fromPath(file);
178+
const ext = picked.extension;
179+
const name = picked.name.replace(`.${ext}`, '');
180+
const tempCopy = File.fromPath(path.join(knownFolders.temp().path, `${name}-copy.${ext}`));
181+
182+
// const done = picked
183+
// .copySync(tempCopy.path);
184+
// console.log('done: ' + done + '\n' + 'Original path: ' + picked.path + '\n' + 'Copied to: ' + tempCopy.path + '\n' + 'Original size: ' + picked.size + '\n' + 'Copy size: ' + tempCopy.size + '\n');
185+
186+
picked
187+
.copy(tempCopy.path)
188+
.then((done) => {
189+
console.log('done: ' + done + '\n' + 'Original path: ' + picked.path + '\n' + 'Copied to: ' + tempCopy.path + '\n' + 'Original size: ' + picked.size + '\n' + 'Copy size: ' + tempCopy.size + '\n');
190+
})
191+
.catch((error) => {
192+
console.log(error);
193+
});
194+
}
195+
196+
export function copyTest() {
197+
const now = Date.now();
198+
const tempFile = File.fromPath(path.join(knownFolders.temp().path, `${now}.txt`));
199+
tempFile.writeTextSync('Hello World: ' + now);
200+
const tempCopy = File.fromPath(path.join(knownFolders.temp().path, `${now}-copy.txt`));
201+
tempFile
202+
.copy(tempCopy.path)
203+
.then((done) => {
204+
console.log('done: ' + done + '\n' + 'Original path: ' + tempFile.path + '\n' + 'Copied to: ' + tempCopy.path + '\n' + 'Original size: ' + tempFile.size + '\n' + 'Copy size: ' + tempCopy.size + '\n');
205+
})
206+
.catch((error) => {
207+
console.log(error);
208+
});
209+
}
210+
175211
function getFileNameFromContent(content: string) {
176212
const file = getFileAccess().getFile(content);
177213
return decodeURIComponent(file.name).split('/').pop().toLowerCase();

apps/toolbox/src/pages/fs-helper.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@
44
<Button text="Create Random" tap="createRandom" />
55
<Button text="Pick File" tap="pickFile" />
66
<Button text="Pick Multiple Files" tap="pickFiles" />
7+
<Button text="Test Copy" tap="copyTest" />
78
</StackLayout>
89
</Page>

packages/core/file-system/file-system-access.android.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,42 @@ export class FileSystemAccess implements IFileSystemAccess {
254254
return this.getLogicalRootPath() + '/app';
255255
}
256256

257+
public copy = this.copySync.bind(this);
258+
259+
public copySync(src: string, dest: string, onError?: (error: any) => any) {
260+
try {
261+
return org.nativescript.widgets.Async.File.copySync(src, dest, getApplicationContext());
262+
} catch (error) {
263+
if (onError) {
264+
onError(exception);
265+
}
266+
}
267+
268+
return false;
269+
}
270+
271+
public copyAsync(src: string, dest: string): Promise<boolean> {
272+
return new Promise<boolean>((resolve, reject) => {
273+
try {
274+
org.nativescript.widgets.Async.File.copy(
275+
src,
276+
dest,
277+
new org.nativescript.widgets.Async.CompleteCallback({
278+
onComplete: (result: boolean) => {
279+
resolve(result);
280+
},
281+
onError: (err) => {
282+
reject(new Error(err));
283+
},
284+
}),
285+
getApplicationContext()
286+
);
287+
} catch (ex) {
288+
reject(ex);
289+
}
290+
});
291+
}
292+
257293
public readBuffer = this.readBufferSync.bind(this);
258294

259295
public readBufferAsync(path: string): Promise<ArrayBuffer> {

packages/core/file-system/file-system-access.d.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,32 @@
22
* An utility class used to provide methods to access and work with the file system.
33
*/
44
export interface IFileSystemAccess {
5+
/**
6+
* Copies a file to a given path.
7+
* @param src The path to the source file.
8+
* @param dest The path to the destination file.
9+
* @param onError (optional) A callback function to use if any error occurs.
10+
* Returns a Promise with a boolean.
11+
*/
12+
copy(src: string, dest: string, onError?: (error: any) => any): any;
13+
14+
/**
15+
* Copies a file to a given path.
16+
* @param src The path to the source file.
17+
* @param dest The path to the destination file.
18+
* Returns a Promise with a boolean.
19+
*/
20+
copyAsync(src: string, dest: string): Promise<any>;
21+
22+
/**
23+
* Copies a file to a given path.
24+
* @param src The path to the source file.
25+
* @param dest The path to the destination file.
26+
* @param onError (optional) A callback function to use if any error occurs.
27+
* Returns a Promise with a boolean.
28+
*/
29+
copySync(src: string, dest: string, onError?: (error: any) => any): any;
30+
531
/**
632
* Gets the last modified date of a file with a given path.
733
* @param path Path to the file.
@@ -256,6 +282,12 @@ export interface IFileSystemAccess {
256282
}
257283

258284
export class FileSystemAccess implements IFileSystemAccess {
285+
copy(src: string, dest: string, onError?: (error: any) => any): boolean;
286+
287+
copySync(src: string, dest: string, onError?: (error: any) => any): boolean;
288+
289+
copyAsync(src: string, dest: string): Promise<boolean>;
290+
259291
getLastModified(path: string): Date;
260292

261293
getFileSize(path: string): number;

packages/core/file-system/file-system-access.ios.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,65 @@ export class FileSystemAccess {
260260
return iOSNativeHelper.getCurrentAppPath();
261261
}
262262

263+
public copy = this.copySync.bind(this);
264+
265+
public copySync(src: string, dest: string, onError?: (error: any) => any) {
266+
const fileManager = NSFileManager.defaultManager;
267+
try {
268+
return fileManager.copyItemAtPathToPathError(src, dest);
269+
} catch (error) {
270+
if (error.message.indexOf('exists') > -1) {
271+
// check the size of file if empty remove then try copying again
272+
// this could be zero due to using File.fromPath passing in a new file
273+
let didRemove = false;
274+
try {
275+
didRemove = fileManager.removeItemAtPathError(dest);
276+
return fileManager.copyItemAtPathToPathError(src, dest);
277+
} catch (error) {
278+
if (onError) {
279+
if (didRemove) {
280+
onError(error);
281+
} else {
282+
onError(exception);
283+
}
284+
}
285+
}
286+
}
287+
if (onError) {
288+
onError(exception);
289+
}
290+
}
291+
292+
return false;
293+
}
294+
295+
public copyAsync(src: string, dest: string): Promise<boolean> {
296+
return new Promise<boolean>((resolve, reject) => {
297+
try {
298+
NSData.dataWithContentsOfFileCompletion(src, (data) => {
299+
if (!data) {
300+
reject(new Error("Failed to read file at path '" + src));
301+
} else {
302+
data.writeToFileAtomicallyCompletion(dest, true, () => {
303+
if (this.fileExists(dest)) {
304+
const size = this.getFileSize(dest);
305+
if (size === data.length) {
306+
resolve(true);
307+
} else {
308+
reject(new Error("Failed to write file at path '" + dest));
309+
}
310+
} else {
311+
reject(new Error("Failed to write file at path '" + dest));
312+
}
313+
});
314+
}
315+
});
316+
} catch (ex) {
317+
reject(ex);
318+
}
319+
});
320+
}
321+
263322
public readText = this.readTextSync.bind(this);
264323

265324
public readTextAsync(path: string, encoding?: any) {

packages/core/file-system/index.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,53 @@ export class File extends FileSystemEntity {
211211
return getFileAccess().getFileSize(this.path);
212212
}
213213

214+
public copy(dest: string): Promise<boolean> {
215+
return new Promise<boolean>((resolve, reject) => {
216+
try {
217+
this._checkAccess();
218+
} catch (ex) {
219+
reject(ex);
220+
221+
return;
222+
}
223+
224+
this._locked = true;
225+
226+
getFileAccess()
227+
.copyAsync(this.path, dest)
228+
.then(
229+
(result) => {
230+
resolve(result);
231+
this._locked = false;
232+
},
233+
(error) => {
234+
reject(error);
235+
this._locked = false;
236+
}
237+
);
238+
});
239+
}
240+
241+
public copySync(dest: string, onError?: (error: any) => any): any {
242+
this._checkAccess();
243+
244+
this._locked = true;
245+
246+
const that = this;
247+
const localError = (error) => {
248+
that._locked = false;
249+
if (onError) {
250+
onError(error);
251+
}
252+
};
253+
254+
const content = getFileAccess().copySync(this.path, dest, localError);
255+
256+
this._locked = false;
257+
258+
return content;
259+
}
260+
214261
public read(): Promise<any> {
215262
return new Promise<any>((resolve, reject) => {
216263
try {
-1.47 KB
Binary file not shown.

packages/types-android/src/lib/android/org.nativescript.widgets.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@
2929
}
3030

3131
export module File {
32+
export function copySync(src: string, dest: string, context: android.content.Context): boolean;
33+
export function copy(src: java.io.InputStream, dest: java.io.OutputStream, callback: org.nativescript.widgets.Async.CompleteCallback, context: any): void;
34+
export function copySync(src: java.io.InputStream, dest: java.io.OutputStream, context: any): boolean;
35+
export function copy(src: string, dest: string, callback: org.nativescript.widgets.Async.CompleteCallback, context: android.content.Context): void;
3236
export function readText(path: string, encoding: string, callback: CompleteCallback, context: any);
3337
export function read(path: string, callback: CompleteCallback, context: any);
3438
export function readBuffer(param0: string, param1: org.nativescript.widgets.Async.CompleteCallback, param2: any): void;

packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/Async.java

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import android.graphics.Bitmap;
66
import android.graphics.BitmapFactory;
77
import android.graphics.drawable.BitmapDrawable;
8+
import android.net.Uri;
89
import android.os.Looper;
910
import android.util.Base64;
1011
import android.util.Log;
@@ -13,6 +14,7 @@
1314
import java.io.ByteArrayOutputStream;
1415
import java.io.Closeable;
1516
import java.io.DataInputStream;
17+
import java.io.File;
1618
import java.io.FileInputStream;
1719
import java.io.FileNotFoundException;
1820
import java.io.FileOutputStream;
@@ -29,6 +31,8 @@
2931
import java.nio.ByteBuffer;
3032
import java.nio.CharBuffer;
3133
import java.nio.channels.FileChannel;
34+
import java.nio.channels.ReadableByteChannel;
35+
import java.nio.channels.WritableByteChannel;
3236
import java.util.ArrayList;
3337
import java.util.List;
3438
import java.util.Locale;
@@ -595,6 +599,94 @@ private void closeOpenedStreams(Stack<Closeable> streams) throws IOException {
595599

596600
public static class File {
597601

602+
public static boolean copySync(final String src, final String dest, final Context context) throws Exception {
603+
InputStream is;
604+
OutputStream os;
605+
606+
if(src.startsWith("content://")){
607+
is = context.getContentResolver().openInputStream(Uri.parse(src));
608+
}else is = new FileInputStream(new java.io.File(src));
609+
610+
if(dest.startsWith("content://")){
611+
os = context.getContentResolver().openOutputStream(Uri.parse(dest));
612+
}else os = new FileOutputStream(new java.io.File(dest));
613+
614+
return copySync(is, os, context);
615+
}
616+
617+
public static boolean copySync(final InputStream src, final OutputStream dest, final Object context) throws Exception {
618+
ReadableByteChannel isc = java.nio.channels.Channels.newChannel(src);
619+
WritableByteChannel osc = java.nio.channels.Channels.newChannel(dest);
620+
621+
int size = src.available();
622+
623+
int written = fastChannelCopy(isc, osc);
624+
625+
return size == written;
626+
}
627+
628+
public static void copy(final String src, final String dest, final CompleteCallback callback, final Context context) {
629+
try {
630+
InputStream is;
631+
OutputStream os;
632+
633+
if(src.startsWith("content://")){
634+
is = context.getContentResolver().openInputStream(Uri.parse(src));
635+
}else is = new FileInputStream(new java.io.File(src));
636+
637+
if(dest.startsWith("content://")){
638+
os = context.getContentResolver().openOutputStream(Uri.parse(dest));
639+
}else os = new FileOutputStream(new java.io.File(dest));
640+
641+
copy(is, os, callback, context);
642+
}catch (Exception exception){
643+
callback.onError(exception.getMessage(), context);
644+
}
645+
}
646+
647+
private static int fastChannelCopy(final ReadableByteChannel src,
648+
final WritableByteChannel dest) throws IOException {
649+
int written = 0;
650+
final ByteBuffer buffer = ByteBuffer.allocateDirect(16 * 1024);
651+
while (src.read(buffer) != -1) {
652+
// prepare the buffer to be drained
653+
buffer.flip();
654+
// write to the channel, may block
655+
written += dest.write(buffer);
656+
// If partial transfer, shift remainder down
657+
// If buffer is empty, same as doing clear()
658+
buffer.compact();
659+
}
660+
// EOF will leave buffer in fill state
661+
buffer.flip();
662+
// make sure the buffer is fully drained.
663+
while (buffer.hasRemaining()) {
664+
written += dest.write(buffer);
665+
}
666+
return written;
667+
}
668+
669+
670+
public static void copy(final InputStream src, final OutputStream dest, final CompleteCallback callback, final Object context) {
671+
final android.os.Handler mHandler = new android.os.Handler(Looper.myLooper());
672+
threadPoolExecutor().execute((Runnable) () -> {
673+
674+
try (InputStream is = src; OutputStream os = dest){
675+
ReadableByteChannel isc = java.nio.channels.Channels.newChannel(is);
676+
WritableByteChannel osc = java.nio.channels.Channels.newChannel(os);
677+
678+
int size = src.available();
679+
680+
int written = fastChannelCopy(isc, osc);
681+
682+
mHandler.post(() -> callback.onComplete(size == written, context));
683+
684+
} catch (Exception e) {
685+
mHandler.post(() -> callback.onError(e.getMessage(), context));
686+
}
687+
});
688+
}
689+
598690
public static void readText(final String path, final String encoding, final CompleteCallback callback, final Object context) {
599691
final android.os.Handler mHandler = new android.os.Handler(Looper.myLooper());
600692
threadPoolExecutor().execute(new Runnable() {

0 commit comments

Comments
 (0)