diff --git a/windows/RNFS.Net46/DiskUtil.cs b/windows/RNFS.Net46/DiskUtil.cs new file mode 100644 index 00000000..612aa374 --- /dev/null +++ b/windows/RNFS.Net46/DiskUtil.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace RNFS +{ + public struct DiskStatus + { + public ulong free; + public ulong total; + } + + public class DiskUtil + { + // Pinvoke for API function + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool GetDiskFreeSpaceEx(string lpDirectoryName, + out ulong lpFreeBytesAvailable, + out ulong lpTotalNumberOfBytes, + out ulong lpTotalNumberOfFreeBytes); + + public static bool DriveFreeBytes(string folderName, out DiskStatus status) + { + if (string.IsNullOrEmpty(folderName)) + { + throw new ArgumentNullException("folderName"); + } + + if (!folderName.EndsWith("\\")) + { + folderName += '\\'; + } + + ulong dummy; + if (GetDiskFreeSpaceEx(folderName, out status.free, out status.total, out dummy)) + { + return true; + } + else + { + return false; + } + } + } +} diff --git a/windows/RNFS.Net46/Properties/AssemblyInfo.cs b/windows/RNFS.Net46/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..3efc649d --- /dev/null +++ b/windows/RNFS.Net46/Properties/AssemblyInfo.cs @@ -0,0 +1,31 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("RNFS")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("RNFS")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: ComVisible(false)] + +[assembly: InternalsVisibleTo("RNFS.Tests")] \ No newline at end of file diff --git a/windows/RNFS.Net46/Properties/RNFS.rd.xml b/windows/RNFS.Net46/Properties/RNFS.rd.xml new file mode 100644 index 00000000..039c5da3 --- /dev/null +++ b/windows/RNFS.Net46/Properties/RNFS.rd.xml @@ -0,0 +1,28 @@ + + + + + + + + + diff --git a/windows/RNFS.Net46/RNFS.Net46.csproj b/windows/RNFS.Net46/RNFS.Net46.csproj new file mode 100644 index 00000000..9292e437 --- /dev/null +++ b/windows/RNFS.Net46/RNFS.Net46.csproj @@ -0,0 +1,71 @@ + + + + + Debug + x64 + {746610D0-8693-11E7-A20D-BF83F7366778} + Library + Properties + RNFS + RNFS + v4.6 + 512 + + + x86 + bin\x86\Debug\ + + + x86 + bin\x86\Release\ + + + x64 + bin\x64\Debug\ + + + x64 + bin\x64\Release\ + + + + False + $(SolutionDir)\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll + True + + + $(SolutionDir)\packages\Syroot.Windows.IO.KnownFolders.1.2.1\lib\net452\Syroot.KnownFolders.dll + + + + + + + + + + + + + + + + + + {22cbff9c-fe36-43e8-a246-266c7635e662} + ReactNative.Net46 + + + + + + + + \ No newline at end of file diff --git a/windows/RNFS.Net46/RNFSManager.cs b/windows/RNFS.Net46/RNFSManager.cs new file mode 100644 index 00000000..3fbb078e --- /dev/null +++ b/windows/RNFS.Net46/RNFSManager.cs @@ -0,0 +1,612 @@ +using Newtonsoft.Json.Linq; +using ReactNative.Bridge; +using ReactNative.Modules.Core; +using ReactNative.Modules.Network; +using Syroot.Windows.IO; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +//using Windows.ApplicationModel; +//using Windows.Storage; + +namespace RNFS +{ + class RNFSManager : ReactContextNativeModuleBase + { + private const int FileType = 0; + private const int DirectoryType = 1; + + private static readonly IReadOnlyDictionary> s_hashAlgorithms = + new Dictionary> + { + { "md5", () => MD5.Create() }, + { "sha1", () => SHA1.Create() }, + { "sha256", () => SHA256.Create() }, + { "sha384", () => SHA384.Create() }, + { "sha512", () => SHA512.Create() }, + }; + + private readonly TaskCancellationManager _tasks = new TaskCancellationManager(); + private readonly HttpClient _httpClient = new HttpClient(); + + private RCTNativeAppEventEmitter _emitter; + + public RNFSManager(ReactContext reactContext) + : base(reactContext) + { + } + + public override string Name + { + get + { + return "RNFSManager"; + } + } + + internal RCTNativeAppEventEmitter Emitter + { + get + { + if (_emitter == null) + { + return Context.GetJavaScriptModule(); + } + + return _emitter; + } + set + { + _emitter = value; + } + } + + [Obsolete] + public override IReadOnlyDictionary Constants + { + get + { + var constants = new Dictionary + { + { "RNFSMainBundlePath", AppDomain.CurrentDomain.BaseDirectory }, + { "RNFSCachesDirectoryPath", KnownFolders.Downloads.Path }, + { "RNFSRoamingDirectoryPath", KnownFolders.RoamingAppData.Path }, + { "RNFSDocumentDirectoryPath", KnownFolders.Documents.Path }, + { "RNFSTemporaryDirectoryPath", KnownFolders.InternetCache.Path }, + { "RNFSPicturesDirectoryPath", KnownFolders.CameraRoll.Path }, + { "RNFSFileTypeRegular", 0 }, + { "RNFSFileTypeDirectory", 1 }, + }; + + return constants; + } + } + + [ReactMethod] + public async void writeFile(string filepath, string base64Content, JObject options, IPromise promise) + { + try + { + // TODO: open file on background thread? + using (var file = File.OpenWrite(filepath)) + { + var data = Convert.FromBase64String(base64Content); + await file.WriteAsync(data, 0, data.Length).ConfigureAwait(false); + } + + promise.Resolve(null); + } + catch (Exception ex) + { + Reject(promise, filepath, ex); + } + } + + [ReactMethod] + public async void appendFile(string filepath, string base64Content, IPromise promise) + { + try + { + // TODO: open file on background thread? + using (var file = File.Open(filepath, FileMode.Append)) + { + var data = Convert.FromBase64String(base64Content); + await file.WriteAsync(data, 0, data.Length).ConfigureAwait(false); + } + + promise.Resolve(null); + } + catch (Exception ex) + { + Reject(promise, filepath, ex); + } + } + + [ReactMethod] + public async void write(string filepath, string base64Content, int position, IPromise promise) + { + try + { + // TODO: open file on background thread? + using (var file = File.OpenWrite(filepath)) + { + if (position >= 0) + { + file.Position = position; + } + + var data = Convert.FromBase64String(base64Content); + await file.WriteAsync(data, 0, data.Length).ConfigureAwait(false); + } + + promise.Resolve(null); + } + catch (Exception ex) + { + Reject(promise, filepath, ex); + } + } + + [ReactMethod] + public void exists(string filepath, IPromise promise) + { + try + { + promise.Resolve(File.Exists(filepath) || Directory.Exists(filepath)); + } + catch (Exception ex) + { + Reject(promise, filepath, ex); + } + } + + [ReactMethod] + public async void readFile(string filepath, IPromise promise) + { + try + { + if (!File.Exists(filepath)) + { + RejectFileNotFound(promise, filepath); + return; + } + + // TODO: open file on background thread? + string base64Content; + using (var file = File.OpenRead(filepath)) + { + var length = (int)file.Length; + var buffer = new byte[length]; + await file.ReadAsync(buffer, 0, length).ConfigureAwait(false); + base64Content = Convert.ToBase64String(buffer); + } + + promise.Resolve(base64Content); + } + catch (Exception ex) + { + Reject(promise, filepath, ex); + } + } + + [ReactMethod] + public async void read(string filepath, int length, int position, IPromise promise) + { + try + { + if (!File.Exists(filepath)) + { + RejectFileNotFound(promise, filepath); + return; + } + + // TODO: open file on background thread? + string base64Content; + using (var file = File.OpenRead(filepath)) + { + file.Position = position; + var buffer = new byte[length]; + await file.ReadAsync(buffer, 0, length).ConfigureAwait(false); + base64Content = Convert.ToBase64String(buffer); + } + + promise.Resolve(base64Content); + } + catch (Exception ex) + { + Reject(promise, filepath, ex); + } + } + + [ReactMethod] + public async void hash(string filepath, string algorithm, IPromise promise) + { + var hashAlgorithmFactory = default(Func); + if (!s_hashAlgorithms.TryGetValue(algorithm, out hashAlgorithmFactory)) + { + promise.Reject(null, "Invalid hash algorithm."); + return; + } + + try + { + if (!File.Exists(filepath)) + { + RejectFileNotFound(promise, filepath); + return; + } + + await Task.Run(() => + { + var hexBuilder = new StringBuilder(); + using (var hashAlgorithm = hashAlgorithmFactory()) + { + hashAlgorithm.Initialize(); + var hash = default(byte[]); + using (var file = File.OpenRead(filepath)) + { + hash = hashAlgorithm.ComputeHash(file); + } + + foreach (var b in hash) + { + hexBuilder.Append(string.Format("{0:x2}", b)); + } + } + + promise.Resolve(hexBuilder.ToString()); + }).ConfigureAwait(false); + } + catch (Exception ex) + { + Reject(promise, filepath, ex); + } + } + + [ReactMethod] + public void moveFile(string filepath, string destPath, JObject options, IPromise promise) + { + try + { + // TODO: move file on background thread? + File.Move(filepath, destPath); + promise.Resolve(true); + } + catch (Exception ex) + { + Reject(promise, filepath, ex); + } + } + + [ReactMethod] + public async void copyFile(string filepath, string destPath, JObject options, IPromise promise) + { + try + { + await Task.Run(() => File.Copy(filepath, destPath)).ConfigureAwait(false); + promise.Resolve(null); + + } + catch (Exception ex) + { + Reject(promise, filepath, ex); + } + } + + [ReactMethod] + public async void readDir(string directory, IPromise promise) + { + try + { + await Task.Run(() => + { + var info = new DirectoryInfo(directory); + if (!info.Exists) + { + promise.Reject(null, "Folder does not exist"); + return; + } + + var fileMaps = new JArray(); + foreach (var item in info.EnumerateFileSystemInfos()) + { + var fileMap = new JObject + { + { "mtime", ConvertToUnixTimestamp(item.LastWriteTime) }, + { "name", item.Name }, + { "path", item.FullName }, + }; + + var fileItem = item as FileInfo; + if (fileItem != null) + { + fileMap.Add("type", FileType); + fileMap.Add("size", fileItem.Length); + } + else + { + fileMap.Add("type", DirectoryType); + fileMap.Add("size", 0); + } + + fileMaps.Add(fileMap); + } + + promise.Resolve(fileMaps); + }); + } + catch (Exception ex) + { + Reject(promise, directory, ex); + } + } + + [ReactMethod] + public void stat(string filepath, IPromise promise) + { + try + { + FileSystemInfo fileSystemInfo = new FileInfo(filepath); + if (!fileSystemInfo.Exists) + { + fileSystemInfo = new DirectoryInfo(filepath); + if (!fileSystemInfo.Exists) + { + promise.Reject(null, "File does not exist."); + return; + } + } + + var fileInfo = fileSystemInfo as FileInfo; + var statMap = new JObject + { + { "ctime", ConvertToUnixTimestamp(fileSystemInfo.CreationTime) }, + { "mtime", ConvertToUnixTimestamp(fileSystemInfo.LastWriteTime) }, + { "size", fileInfo?.Length ?? 0 }, + { "type", fileInfo != null ? FileType: DirectoryType }, + }; + + promise.Resolve(statMap); + } + catch (Exception ex) + { + Reject(promise, filepath, ex); + } + } + + [ReactMethod] + public async void unlink(string filepath, IPromise promise) + { + try + { + var directoryInfo = new DirectoryInfo(filepath); + var fileInfo = default(FileInfo); + if (directoryInfo.Exists) + { + await Task.Run(() => Directory.Delete(filepath, true)).ConfigureAwait(false); + } + else if ((fileInfo = new FileInfo(filepath)).Exists) + { + await Task.Run(() => File.Delete(filepath)).ConfigureAwait(false); + } + else + { + promise.Reject(null, "File does not exist."); + return; + } + + promise.Resolve(null); + } + catch (Exception ex) + { + Reject(promise, filepath, ex); + } + } + + [ReactMethod] + public async void mkdir(string filepath, JObject options, IPromise promise) + { + try + { + await Task.Run(() => Directory.CreateDirectory(filepath)).ConfigureAwait(false); + promise.Resolve(null); + } + catch (Exception ex) + { + Reject(promise, filepath, ex); + } + } + + [ReactMethod] + public async void downloadFile(JObject options, IPromise promise) + { + var filepath = options.Value("toFile"); + + try + { + var url = new Uri(options.Value("fromUrl")); + var jobId = options.Value("jobId"); + var headers = (JObject)options["headers"]; + var progressDivider = options.Value("progressDivider"); + + var request = new HttpRequestMessage(HttpMethod.Get, url); + foreach (var header in headers) + { + request.Headers.Add(header.Key, header.Value.Value()); + } + + await _tasks.AddAndInvokeAsync(jobId, token => + ProcessRequestAsync(promise, request, filepath, jobId, progressDivider, token)); + } + catch (Exception ex) + { + Reject(promise, filepath, ex); + } + } + + [ReactMethod] + public void stopDownload(int jobId) + { + _tasks.Cancel(jobId); + } + + [ReactMethod] + public async void getFSInfo(IPromise promise) + { + try + { + DiskStatus status = new DiskStatus(); + DiskUtil.DriveFreeBytes(KnownFolders.RoamingAppData.Path, out status); + promise.Resolve(new JObject + { + { "freeSpace", status.free }, + { "totalSpace", status.total }, + }); + } + catch (Exception) + { + promise.Reject(null, "getFSInfo is not available"); + } + } + + [ReactMethod] + public async void touch(string filepath, double mtime, double ctime, IPromise promise) + { + try + { + await Task.Run(() => + { + var fileInfo = new FileInfo(filepath); + if (!fileInfo.Exists) + { + using (File.Create(filepath)) { } + } + + fileInfo.CreationTimeUtc = ConvertFromUnixTimestamp(ctime); + fileInfo.LastWriteTimeUtc = ConvertFromUnixTimestamp(mtime); + + promise.Resolve(fileInfo.FullName); + }); + } + catch (Exception ex) + { + Reject(promise, filepath, ex); + } + } + + public override void OnReactInstanceDispose() + { + _tasks.CancelAllTasks(); + _httpClient.Dispose(); + } + + private async Task ProcessRequestAsync(IPromise promise, HttpRequestMessage request, string filepath, int jobId, int progressIncrement, CancellationToken token) + { + try + { + using (var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token)) + { + var headersMap = new JObject(); + foreach (var header in response.Headers) + { + headersMap.Add(header.Key, string.Join(",", header.Value)); + } + + var contentLength = response.Content.Headers.ContentLength; + SendEvent($"DownloadBegin-{jobId}", new JObject + { + { "jobId", jobId }, + { "statusCode", (int)response.StatusCode }, + { "contentLength", contentLength }, + { "headers", headersMap }, + }); + + // TODO: open file on background thread? + long totalRead = 0; + using (var fileStream = File.OpenWrite(filepath)) + using (var stream = await response.Content.ReadAsStreamAsync()) + { + var contentLengthForProgress = contentLength ?? -1; + var nextProgressIncrement = progressIncrement; + var buffer = new byte[8 * 1024]; + var read = 0; + while ((read = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0) + { + token.ThrowIfCancellationRequested(); + + await fileStream.WriteAsync(buffer, 0, read); + if (contentLengthForProgress >= 0) + { + totalRead += read; + if (totalRead * 100 / contentLengthForProgress >= nextProgressIncrement || + totalRead == contentLengthForProgress) + { + SendEvent("DownloadProgress-" + jobId, new JObject + { + { "jobId", jobId }, + { "contentLength", contentLength }, + { "bytesWritten", totalRead }, + }); + + nextProgressIncrement += progressIncrement; + } + } + } + } + + promise.Resolve(new JObject + { + { "jobId", jobId }, + { "statusCode", (int)response.StatusCode }, + { "bytesWritten", totalRead }, + }); + } + } + finally + { + request.Dispose(); + } + } + + private void Reject(IPromise promise, String filepath, Exception ex) + { + if (ex is FileNotFoundException) { + RejectFileNotFound(promise, filepath); + return; + } + + promise.Reject(ex); + } + + private void RejectFileNotFound(IPromise promise, String filepath) + { + promise.Reject("ENOENT", "ENOENT: no such file or directory, open '" + filepath + "'"); + } + + private void SendEvent(string eventName, JObject eventData) + { + Emitter.emit(eventName, eventData); + } + + public static double ConvertToUnixTimestamp(DateTime date) + { + var origin = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + var diff = date.ToUniversalTime() - origin; + return Math.Floor(diff.TotalSeconds); + } + + public static DateTime ConvertFromUnixTimestamp(double timestamp) + { + var origin = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + var diff = TimeSpan.FromSeconds(timestamp); + var dateTimeUtc = origin + diff; + return dateTimeUtc.ToLocalTime(); + } + } +} diff --git a/windows/RNFS.Net46/RNFSPackage.cs b/windows/RNFS.Net46/RNFSPackage.cs new file mode 100644 index 00000000..ecd67ccb --- /dev/null +++ b/windows/RNFS.Net46/RNFSPackage.cs @@ -0,0 +1,53 @@ +using ReactNative.Bridge; +using ReactNative.Modules.Core; +using ReactNative.UIManager; +using System; +using System.Collections.Generic; + +namespace RNFS +{ + /// + /// Package defining core framework modules (e.g., ). + /// It should be used for modules that require special integration with + /// other framework parts (e.g., with the list of packages to load view + /// managers from). + /// + public class RNFSPackage : IReactPackage + { + /// + /// Creates the list of native modules to register with the react + /// instance. + /// + /// The react application context. + /// The list of native modules. + public IReadOnlyList CreateNativeModules(ReactContext reactContext) + { + return new List + { + new RNFSManager(reactContext), + }; + } + + /// + /// Creates the list of JavaScript modules to register with the + /// react instance. + /// + /// The list of JavaScript modules. + public IReadOnlyList CreateJavaScriptModulesConfig() + { + return new List(0); + } + + /// + /// Creates the list of view managers that should be registered with + /// the . + /// + /// The react application context. + /// The list of view managers. + public IReadOnlyList CreateViewManagers( + ReactContext reactContext) + { + return new List(0); + } + } +} diff --git a/windows/RNFS.Net46/packages.config b/windows/RNFS.Net46/packages.config new file mode 100644 index 00000000..b427c04e --- /dev/null +++ b/windows/RNFS.Net46/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/windows/RNFS.sln b/windows/RNFS.sln index f8475f26..4d8dc9a3 100644 --- a/windows/RNFS.sln +++ b/windows/RNFS.sln @@ -12,6 +12,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ChakraBridge", "..\node_mod EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RNFS.Tests", "RNFS.Tests\RNFS.Tests.csproj", "{8D2229AC-F6EC-4FBD-9AAC-FE4792DA98C6}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RNFS.Net46", "RNFS.Net46\RNFS.Net46.csproj", "{8F7EE18F-8E79-4648-B442-9554443BE262}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution ..\node_modules\react-native-windows\ReactWindows\ReactNative.Shared\ReactNative.Shared.projitems*{c7673ad5-e3aa-468c-a5fd-fa38154e205c}*SharedItemsImports = 4 @@ -111,6 +113,24 @@ Global {8D2229AC-F6EC-4FBD-9AAC-FE4792DA98C6}.Release|x86.ActiveCfg = Release|x86 {8D2229AC-F6EC-4FBD-9AAC-FE4792DA98C6}.Release|x86.Build.0 = Release|x86 {8D2229AC-F6EC-4FBD-9AAC-FE4792DA98C6}.Release|x86.Deploy.0 = Release|x86 + {8F7EE18F-8E79-4648-B442-9554443BE262}.Debug|ARM.ActiveCfg = Debug|ARM + {8F7EE18F-8E79-4648-B442-9554443BE262}.Debug|ARM.Build.0 = Debug|ARM + {8F7EE18F-8E79-4648-B442-9554443BE262}.Debug|x64.ActiveCfg = Debug|x64 + {8F7EE18F-8E79-4648-B442-9554443BE262}.Debug|x64.Build.0 = Debug|x64 + {8F7EE18F-8E79-4648-B442-9554443BE262}.Debug|x86.ActiveCfg = Debug|x86 + {8F7EE18F-8E79-4648-B442-9554443BE262}.Debug|x86.Build.0 = Debug|x86 + {8F7EE18F-8E79-4648-B442-9554443BE262}.Development|ARM.ActiveCfg = Development|ARM + {8F7EE18F-8E79-4648-B442-9554443BE262}.Development|ARM.Build.0 = Development|ARM + {8F7EE18F-8E79-4648-B442-9554443BE262}.Development|x64.ActiveCfg = Development|x64 + {8F7EE18F-8E79-4648-B442-9554443BE262}.Development|x64.Build.0 = Development|x64 + {8F7EE18F-8E79-4648-B442-9554443BE262}.Development|x86.ActiveCfg = Development|x86 + {8F7EE18F-8E79-4648-B442-9554443BE262}.Development|x86.Build.0 = Development|x86 + {8F7EE18F-8E79-4648-B442-9554443BE262}.Release|ARM.ActiveCfg = Release|ARM + {8F7EE18F-8E79-4648-B442-9554443BE262}.Release|ARM.Build.0 = Release|ARM + {8F7EE18F-8E79-4648-B442-9554443BE262}.Release|x64.ActiveCfg = Release|x64 + {8F7EE18F-8E79-4648-B442-9554443BE262}.Release|x64.Build.0 = Release|x64 + {8F7EE18F-8E79-4648-B442-9554443BE262}.Release|x86.ActiveCfg = Release|x86 + {8F7EE18F-8E79-4648-B442-9554443BE262}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE