├── Samples └── AndroidSample │ ├── Resources │ ├── values │ │ ├── styles.xml │ │ ├── dimens.xml │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── colors.xml │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ └── ic_launcher_foreground.png │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ └── ic_launcher_foreground.png │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ └── ic_launcher_foreground.png │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ └── ic_launcher_foreground.png │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ └── ic_launcher_foreground.png │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ └── AboutResources.txt │ ├── Assets │ ├── Test.zip │ └── AboutAssets.txt │ ├── Properties │ ├── AndroidManifest.xml │ └── AssemblyInfo.cs │ ├── MainActivity.cs │ └── AndroidSample.csproj ├── PortableStorage.Test ├── Resources │ └── Test.zip ├── PortableStorage.Test.csproj ├── Resource.Designer.cs ├── BufferedStreamTest.cs ├── Resource.resx ├── ZipStorageTest.cs └── StorageTest.cs ├── PortableStorage ├── StreamMode.cs ├── StreamAccess.cs ├── StorageEntryRequest.cs ├── StreamShare.cs ├── StreamAttributes.cs ├── Providers │ ├── CreateStorageResult.cs │ ├── CreateStreamResult.cs │ ├── IVirtualStorageProvider.cs │ ├── ZipVirualStorageProvider.cs │ ├── IStorageProvider.cs │ ├── FileStorgeProvider.cs │ └── ZipStorgeProvider.cs ├── StorageEntryBase.cs ├── GlobalSuppressions.cs ├── PathUtil.cs ├── PortableStorage.nuspec ├── StorageOptions.cs ├── Exceptions │ ├── StorageNotEnoughSpaceException.cs │ └── StorageNotFoundException.cs ├── StorageRoot.cs ├── PortableStorage.csproj ├── StreamController.cs ├── StorageEntry.cs ├── SyncStream.cs ├── AesStream.cs ├── BufferedStream.cs └── Storage.cs ├── Publish.bat ├── PortableStorage.Android ├── PortableStorage.Android.nuspec ├── Resources │ └── Resource.designer.cs ├── Properties │ └── AssemblyInfo.cs ├── SafStorageHelper.cs ├── PortableStorage.Android.csproj ├── ChannelStream.cs └── SAFStorgeProvider.cs ├── LICENSE ├── README.md ├── PortableStorage.sln └── .gitignore /Samples/AndroidSample/Resources/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Samples/AndroidSample/Resources/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 16dp 3 | 4 | -------------------------------------------------------------------------------- /Samples/AndroidSample/Assets/Test.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madnik7/PortableStorage/HEAD/Samples/AndroidSample/Assets/Test.zip -------------------------------------------------------------------------------- /PortableStorage.Test/Resources/Test.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madnik7/PortableStorage/HEAD/PortableStorage.Test/Resources/Test.zip -------------------------------------------------------------------------------- /Samples/AndroidSample/Resources/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madnik7/PortableStorage/HEAD/Samples/AndroidSample/Resources/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /Samples/AndroidSample/Resources/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madnik7/PortableStorage/HEAD/Samples/AndroidSample/Resources/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /Samples/AndroidSample/Resources/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madnik7/PortableStorage/HEAD/Samples/AndroidSample/Resources/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /Samples/AndroidSample/Resources/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madnik7/PortableStorage/HEAD/Samples/AndroidSample/Resources/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /Samples/AndroidSample/Resources/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madnik7/PortableStorage/HEAD/Samples/AndroidSample/Resources/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /PortableStorage/StreamMode.cs: -------------------------------------------------------------------------------- 1 | namespace PortableStorage 2 | { 3 | public enum StreamMode 4 | { 5 | Append, 6 | Open, 7 | Truncate, 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Samples/AndroidSample/Resources/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madnik7/PortableStorage/HEAD/Samples/AndroidSample/Resources/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /Samples/AndroidSample/Resources/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madnik7/PortableStorage/HEAD/Samples/AndroidSample/Resources/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /Samples/AndroidSample/Resources/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madnik7/PortableStorage/HEAD/Samples/AndroidSample/Resources/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /Samples/AndroidSample/Resources/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madnik7/PortableStorage/HEAD/Samples/AndroidSample/Resources/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /Samples/AndroidSample/Resources/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madnik7/PortableStorage/HEAD/Samples/AndroidSample/Resources/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /Samples/AndroidSample/Resources/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #2C3E50 4 | -------------------------------------------------------------------------------- /Samples/AndroidSample/Resources/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | AndroidSample 3 | Settings 4 | 5 | -------------------------------------------------------------------------------- /Samples/AndroidSample/Resources/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madnik7/PortableStorage/HEAD/Samples/AndroidSample/Resources/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /Samples/AndroidSample/Resources/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madnik7/PortableStorage/HEAD/Samples/AndroidSample/Resources/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /Samples/AndroidSample/Resources/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madnik7/PortableStorage/HEAD/Samples/AndroidSample/Resources/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /Samples/AndroidSample/Resources/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madnik7/PortableStorage/HEAD/Samples/AndroidSample/Resources/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /Publish.bat: -------------------------------------------------------------------------------- 1 | SET curdir=%~dp0 2 | 3 | start /b cmd /k call "%curdir%PortableStorage\.nuget\publish.bat" 4 | pause 5 | 6 | start /b cmd /k call "%curdir%PortableStorage.Android\.nuget\publish.bat" 7 | 8 | -------------------------------------------------------------------------------- /Samples/AndroidSample/Resources/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madnik7/PortableStorage/HEAD/Samples/AndroidSample/Resources/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /PortableStorage/StreamAccess.cs: -------------------------------------------------------------------------------- 1 | namespace PortableStorage 2 | { 3 | public enum StreamAccess 4 | { 5 | Read =0x1, 6 | Write =0x2, 7 | ReadWrite =0x4, 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /PortableStorage/StorageEntryRequest.cs: -------------------------------------------------------------------------------- 1 | namespace PortableStorage 2 | { 3 | public class StorageEntryRequest 4 | { 5 | public Storage Storage {get;set;} 6 | public string Path {get;set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /PortableStorage/StreamShare.cs: -------------------------------------------------------------------------------- 1 | namespace PortableStorage 2 | { 3 | public enum StreamShare 4 | { 5 | None = 0x0, 6 | Read = 0x1, 7 | Write = 0x2, 8 | ReadWrite = 0x4 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /PortableStorage/StreamAttributes.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PortableStorage 4 | { 5 | [Flags] 6 | public enum StreamAttributes 7 | { 8 | Hidden = 0x001, 9 | System = 0x002 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /PortableStorage/Providers/CreateStorageResult.cs: -------------------------------------------------------------------------------- 1 | namespace PortableStorage.Providers 2 | { 3 | public class CreateStorageResult 4 | { 5 | public StorageEntryBase Entry { get; set; } 6 | public IStorageProvider Storage { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Samples/AndroidSample/Resources/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #2c3e50 4 | #1B3147 5 | #3498db 6 | 7 | -------------------------------------------------------------------------------- /PortableStorage/Providers/CreateStreamResult.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace PortableStorage.Providers 4 | { 5 | public class CreateStreamResult 6 | { 7 | public StorageEntryBase EntryBase { get; set; } 8 | public Stream Stream { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Samples/AndroidSample/Resources/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Samples/AndroidSample/Resources/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /PortableStorage/Providers/IVirtualStorageProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | 5 | namespace PortableStorage.Providers 6 | { 7 | public interface IVirtualStorageProvider 8 | { 9 | IStorageProvider CreateStorageProvider(Stream stream, Uri streamUri, string streamName); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /PortableStorage/Providers/ZipVirualStorageProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace PortableStorage.Providers 5 | { 6 | public class ZipVirualStorageProvider : IVirtualStorageProvider 7 | { 8 | public IStorageProvider CreateStorageProvider(Stream stream, Uri streamUri, string streamName) 9 | { 10 | return new ZipStorgeProvider(stream, streamUri, streamName); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /PortableStorage/StorageEntryBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PortableStorage 4 | { 5 | public class StorageEntryBase 6 | { 7 | public Uri Uri { get; set; } 8 | public string Name { get; set; } 9 | public long Size { get; set; } 10 | public DateTime? LastWriteTime { get; set; } 11 | public StreamAttributes Attributes { get; set; } 12 | public bool IsStorage { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Samples/AndroidSample/Properties/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Samples/AndroidSample/Assets/AboutAssets.txt: -------------------------------------------------------------------------------- 1 | Any raw assets you want to be deployed with your application can be placed in 2 | this directory (and child directories) and given a Build Action of "AndroidAsset". 3 | 4 | These files will be deployed with you package and will be accessible using Android's 5 | AssetManager, like this: 6 | 7 | public class ReadAsset : Activity 8 | { 9 | protected override void OnCreate (Bundle bundle) 10 | { 11 | base.OnCreate (bundle); 12 | 13 | InputStream input = Assets.Open ("my_asset.txt"); 14 | } 15 | } 16 | 17 | Additionally, some Android functions will automatically load asset files: 18 | 19 | Typeface tf = Typeface.CreateFromAsset (Context.Assets, "fonts/samplefont.ttf"); -------------------------------------------------------------------------------- /PortableStorage/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | // This file is used by Code Analysis to maintain SuppressMessage 2 | // attributes that are applied to this project. 3 | // Project-level suppressions either have no target or are given 4 | // a specific target and scoped to a namespace, type, member, etc. 5 | 6 | using System.Diagnostics.CodeAnalysis; 7 | 8 | [assembly: SuppressMessage("Naming", "CA1721:Property names should not match get methods", Justification = "", Scope = "module")] 9 | [assembly: SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "", Scope = "module")] 10 | [assembly: SuppressMessage("Usage", "CA1816:Dispose methods should call SuppressFinalize", Justification = "", Scope = "module")] 11 | -------------------------------------------------------------------------------- /PortableStorage/PathUtil.cs: -------------------------------------------------------------------------------- 1 | 2 | using System; 3 | 4 | namespace PortableStorage 5 | { 6 | public static class PathUtil 7 | { 8 | public static string RemoveLastSeparator(string path) 9 | { 10 | if (path == null) throw new ArgumentNullException(nameof(path)); 11 | 12 | path = path.Replace('\\', Storage.SeparatorChar); 13 | return path.TrimEnd(Storage.SeparatorChar); 14 | } 15 | 16 | public static string AddLastSeparator(string path) 17 | { 18 | if (path == null) throw new ArgumentNullException(nameof(path)); 19 | 20 | path = path.Replace('\\', Storage.SeparatorChar); 21 | return path.TrimEnd(Storage.SeparatorChar) + '/'; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /PortableStorage/PortableStorage.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PortableStorage 5 | 1.2.70 6 | Mohammad Nikravan 7 | Mohammad Nikravan 8 | false 9 | A Portable Storage for .NET 10 | IsolatedStorage PortableStorage Storage 11 | 12 | https://github.com/madnik7/PortableStorage 13 | MIT 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /PortableStorage/StorageOptions.cs: -------------------------------------------------------------------------------- 1 | using PortableStorage.Providers; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Collections.ObjectModel; 5 | 6 | namespace PortableStorage 7 | { 8 | public class StorageOptions 9 | { 10 | public StorageOptions() 11 | { 12 | VirtualStorageProviders = new Dictionary(StringComparer.OrdinalIgnoreCase) 13 | { 14 | { ".zip", new ZipVirualStorageProvider() } 15 | }; 16 | } 17 | 18 | public IDictionary VirtualStorageProviders { get; } 19 | public int CacheTimeout { get; set; } = -1; 20 | public bool IgnoreCase { get; set; } = true; 21 | public bool LeaveProviderOpen { get; set; } = false; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /PortableStorage/Exceptions/StorageNotEnoughSpaceException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PortableStorage.Exceptions 4 | { 5 | public class StorageNotEnoughSpaceException : Exception 6 | { 7 | public long MinSpace { get; private set; } 8 | public StorageNotEnoughSpaceException(long minSpace) 9 | : base($"Free space is too low. It must be more than: {minSpace / 1000000} MB!") 10 | { 11 | MinSpace = minSpace; 12 | } 13 | 14 | public StorageNotEnoughSpaceException() 15 | { 16 | } 17 | 18 | public StorageNotEnoughSpaceException(string message) : base(message) 19 | { 20 | } 21 | 22 | public StorageNotEnoughSpaceException(string message, Exception innerException) : base(message, innerException) 23 | { 24 | } 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /PortableStorage.Android/PortableStorage.Android.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PortableStorage.Android 5 | 1.2.71 6 | Mohammad Nikravan 7 | Mohammad Nikravan 8 | false 9 | A Portable Storage Provider for Android Storage Access Framework (SAF) 10 | IsolatedStorage PortableStorage Storage Android SAF Storage-Access-Framework Xamarin 11 | 12 | https://github.com/madnik7/PortableStorage 13 | MIT 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /PortableStorage/Exceptions/StorageNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace PortableStorage.Exceptions 5 | { 6 | public class StorageNotFoundException : FileNotFoundException 7 | { 8 | public StorageNotFoundException(Uri storageUri, string entryName) 9 | : base($"Storage Entry not found! storageUri: {storageUri}, entryName:{entryName}") 10 | { 11 | } 12 | 13 | public StorageNotFoundException(Uri uri) 14 | : base($"Storage Entry not found! Uri: {uri}") 15 | { 16 | } 17 | 18 | public StorageNotFoundException() 19 | { 20 | } 21 | 22 | public StorageNotFoundException(string message) : base(message) 23 | { 24 | } 25 | 26 | public StorageNotFoundException(string message, Exception innerException) : base(message, innerException) 27 | { 28 | } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /PortableStorage/StorageRoot.cs: -------------------------------------------------------------------------------- 1 | using PortableStorage.Providers; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace PortableStorage 7 | { 8 | public class StorageRoot : Storage, IDisposable 9 | { 10 | public StorageRoot(IStorageProvider provider, StorageOptions options) : 11 | base(provider, options) 12 | { 13 | } 14 | 15 | #region IDisposable Support 16 | protected virtual void Dispose(bool disposing) 17 | { 18 | if (disposing) 19 | Close(); 20 | } 21 | 22 | // free unmanaged resources (unmanaged objects) and override a finalizer below. 23 | // set large fields to null. 24 | 25 | // This code added to correctly implement the disposable pattern. 26 | public void Dispose() 27 | { 28 | // Do not change this code. Put cleanup code in Dispose(bool disposing) above. 29 | Dispose(true); 30 | } 31 | #endregion 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /PortableStorage/Providers/IStorageProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | 5 | namespace PortableStorage.Providers 6 | { 7 | public interface IStorageProvider : IDisposable 8 | { 9 | Uri Uri { get; } 10 | string Name { get; } 11 | bool IsGetEntriesBySearchPatternFast { get; } 12 | bool IsGetEntryUriByNameFast { get; } 13 | long GetFreeSpace(); 14 | CreateStorageResult CreateStorage(string name); 15 | CreateStreamResult CreateStream(string name, StreamAccess access, StreamShare share, int bufferSize); 16 | IStorageProvider OpenStorage(Uri uri); 17 | Stream OpenStream(Uri uri, StreamMode mode, StreamAccess access, StreamShare share, int bufferSize); 18 | void RemoveStream(Uri uri); 19 | void RemoveStorage(Uri uri); 20 | Uri Rename(Uri uri, string desName); 21 | void SetAttributes(Uri uri, StreamAttributes attributes); 22 | StorageEntryBase[] GetEntries(string searchPattern = null); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /PortableStorage/PortableStorage.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | Mohammad Nikravan 6 | IsolatedStorage PortableStorage Storage 7 | 1.0.10 8 | https://github.com/madnik7/PortableStorage 9 | https://github.com/madnik7/PortableStorage 10 | 11 | A Portable Storage for .NET 12 | true 13 | MIT 14 | 15 | 16 | 17 | 18 | 19 | all 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Mohammad Nikravan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PortableStorage.Test/PortableStorage.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | false 7 | 8 | 8.0 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | True 24 | True 25 | Resource.resx 26 | 27 | 28 | 29 | 30 | 31 | ResXFileCodeGenerator 32 | Resource.Designer.cs 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /PortableStorage.Android/Resources/Resource.designer.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable 1591 2 | //------------------------------------------------------------------------------ 3 | // 4 | // This code was generated by a tool. 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | [assembly: global::Android.Runtime.ResourceDesignerAttribute("PortableStorage.Droid.Resource", IsApplication=false)] 12 | 13 | namespace PortableStorage.Droid 14 | { 15 | 16 | 17 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "1.0.0.0")] 18 | public partial class Resource 19 | { 20 | 21 | static Resource() 22 | { 23 | global::Android.Runtime.ResourceIdManager.UpdateIdValues(); 24 | } 25 | 26 | public partial class Attribute 27 | { 28 | 29 | static Attribute() 30 | { 31 | global::Android.Runtime.ResourceIdManager.UpdateIdValues(); 32 | } 33 | 34 | private Attribute() 35 | { 36 | } 37 | } 38 | } 39 | } 40 | #pragma warning restore 1591 41 | -------------------------------------------------------------------------------- /Samples/AndroidSample/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | using Android.App; 5 | 6 | // General Information about an assembly is controlled through the following 7 | // set of attributes. Change these attribute values to modify the information 8 | // associated with an assembly. 9 | [assembly: AssemblyTitle("AndroidSample")] 10 | [assembly: AssemblyDescription("")] 11 | [assembly: AssemblyConfiguration("")] 12 | [assembly: AssemblyCompany("")] 13 | [assembly: AssemblyProduct("AndroidSample")] 14 | [assembly: AssemblyCopyright("Copyright © 2018")] 15 | [assembly: AssemblyTrademark("")] 16 | [assembly: AssemblyCulture("")] 17 | [assembly: ComVisible(false)] 18 | 19 | // Version information for an assembly consists of the following four values: 20 | // 21 | // Major Version 22 | // Minor Version 23 | // Build Number 24 | // Revision 25 | // 26 | // You can specify all the values or you can default the Build and Revision Numbers 27 | // by using the '*' as shown below: 28 | // [assembly: AssemblyVersion("1.0.*")] 29 | [assembly: AssemblyVersion("1.0.0.0")] 30 | [assembly: AssemblyFileVersion("1.0.0.0")] 31 | -------------------------------------------------------------------------------- /PortableStorage.Android/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | using Android.App; 5 | 6 | // General Information about an assembly is controlled through the following 7 | // set of attributes. Change these attribute values to modify the information 8 | // associated with an assembly. 9 | [assembly: AssemblyTitle("PortableStorage.Android")] 10 | [assembly: AssemblyDescription("")] 11 | [assembly: AssemblyConfiguration("")] 12 | [assembly: AssemblyCompany("")] 13 | [assembly: AssemblyProduct("PortableStorage.Android")] 14 | [assembly: AssemblyCopyright("Copyright © 2019")] 15 | [assembly: AssemblyTrademark("")] 16 | [assembly: AssemblyCulture("")] 17 | [assembly: ComVisible(false)] 18 | 19 | // Version information for an assembly consists of the following four values: 20 | // 21 | // Major Version 22 | // Minor Version 23 | // Build Number 24 | // Revision 25 | // 26 | // You can specify all the values or you can default the Build and Revision Numbers 27 | // by using the '*' as shown below: 28 | // [assembly: AssemblyVersion("1.0.*")] 29 | [assembly: AssemblyVersion("1.0.0.0")] 30 | [assembly: AssemblyFileVersion("1.0.0.0")] 31 | -------------------------------------------------------------------------------- /PortableStorage.Android/SafStorageHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using Android.App; 4 | using Android.Content; 5 | using Android.Provider; 6 | 7 | namespace PortableStorage.Droid 8 | { 9 | public static class SafStorageHelper 10 | { 11 | public static void BrowserFolder(Activity activity, int requestCode) 12 | { 13 | using var intent = new Intent(Intent.ActionOpenDocumentTree); 14 | intent.PutExtra("android.content.extra.SHOW_ADVANCED", true); 15 | intent.PutExtra("android.content.extra.FANCY", true); 16 | activity.StartActivityForResult(intent, requestCode); 17 | } 18 | 19 | /// 20 | /// return null if the request does not belong to requestId 21 | /// 22 | public static Uri ResolveFromActivityResult(Activity activity, Intent data) 23 | { 24 | var androidUri = data.Data; 25 | var takeFlags = data.Flags & (ActivityFlags.GrantReadUriPermission | ActivityFlags.GrantWriteUriPermission); 26 | activity.ContentResolver.TakePersistableUriPermission(androidUri, takeFlags); 27 | var storageUri = DocumentsContract.BuildDocumentUriUsingTree(androidUri, DocumentsContract.GetTreeDocumentId(androidUri)); 28 | return new Uri(storageUri.ToString()); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /PortableStorage/StreamController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Text; 5 | 6 | namespace PortableStorage 7 | { 8 | internal class StreamController : Stream 9 | { 10 | private readonly Stream _stream; 11 | private readonly StorageEntry _entry; 12 | public StreamController(Stream stream, StorageEntry entry) 13 | { 14 | _stream = stream; 15 | _entry = entry; 16 | } 17 | 18 | public override bool CanRead => _stream.CanRead; 19 | 20 | public override bool CanSeek => _stream.CanSeek; 21 | 22 | public override bool CanWrite => _stream.CanWrite; 23 | 24 | public override long Length => _stream.Length; 25 | 26 | public override long Position { get => _stream.Position; set => _stream.Position = value; } 27 | public override int Read(byte[] buffer, int offset, int count) => _stream.Read(buffer, offset, count); 28 | public override long Seek(long offset, SeekOrigin origin) => _stream.Seek(offset, origin); 29 | public override void Flush() => _stream.Flush(); 30 | protected override void Dispose(bool disposing) => _stream.Dispose(); 31 | public override void Close() => _stream.Close(); 32 | 33 | public override void SetLength(long value) 34 | { 35 | _stream.SetLength(value); 36 | _entry.Size = value; 37 | _entry.LastWriteTime = DateTime.Now; 38 | } 39 | 40 | public override void Write(byte[] buffer, int offset, int count) 41 | { 42 | _stream.Write(buffer, offset, count); 43 | _entry.Size += count; 44 | _entry.LastWriteTime = DateTime.Now; 45 | } 46 | 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Samples/AndroidSample/Resources/AboutResources.txt: -------------------------------------------------------------------------------- 1 | Images, layout descriptions, binary blobs and string dictionaries can be included 2 | in your application as resource files. Various Android APIs are designed to 3 | operate on the resource IDs instead of dealing with images, strings or binary blobs 4 | directly. 5 | 6 | For example, a sample Android app that contains a user interface layout (main.axml), 7 | an internationalization string table (strings.xml) and some icons (drawable-XXX/icon.png) 8 | would keep its resources in the "Resources" directory of the application: 9 | 10 | Resources/ 11 | drawable/ 12 | icon.png 13 | 14 | layout/ 15 | main.axml 16 | 17 | values/ 18 | strings.xml 19 | 20 | In order to get the build system to recognize Android resources, set the build action to 21 | "AndroidResource". The native Android APIs do not operate directly with filenames, but 22 | instead operate on resource IDs. When you compile an Android application that uses resources, 23 | the build system will package the resources for distribution and generate a class called "R" 24 | (this is an Android convention) that contains the tokens for each one of the resources 25 | included. For example, for the above Resources layout, this is what the R class would expose: 26 | 27 | public class R { 28 | public class drawable { 29 | public const int icon = 0x123; 30 | } 31 | 32 | public class layout { 33 | public const int main = 0x456; 34 | } 35 | 36 | public class strings { 37 | public const int first_string = 0xabc; 38 | public const int second_string = 0xbcd; 39 | } 40 | } 41 | 42 | You would then use R.drawable.icon to reference the drawable/icon.png file, or R.layout.main 43 | to reference the layout/main.axml file, or R.strings.first_string to reference the first 44 | string in the dictionary file values/strings.xml. -------------------------------------------------------------------------------- /PortableStorage/StorageEntry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | 5 | namespace PortableStorage 6 | { 7 | public class StorageEntry : StorageEntryBase 8 | { 9 | internal StorageRoot Root { get; set; } 10 | private Storage StreamParent => IsStream ? Parent : throw new InvalidOperationException("Invalid operation for a stream entry!"); 11 | 12 | public bool IsVirtualStorage { get; internal set; } 13 | public Storage Parent { get; internal set; } 14 | public string Path { get; internal set; } 15 | public bool IsStream { get; internal set; } 16 | public bool IsHidden => Attributes.HasFlag(StreamAttributes.Hidden) || (!string.IsNullOrEmpty(Name) && Name[0] == '.'); 17 | public bool Exists => Root != null || Parent.TryGetEntry(Name, out StorageEntry entry) && entry.IsStorage == IsStorage; 18 | public Storage OpenStorage() => Root ?? Parent.OpenStorage(Name); 19 | public Stream OpenStreamRead() => StreamParent.OpenStreamRead(Name); 20 | public Stream OpenStreamWrite(bool truncate) => StreamParent.OpenStreamWrite(Name, truncate); 21 | public byte[] ReadAllBytes() => StreamParent.ReadAllBytes(Name); 22 | public string ReadAllText() => StreamParent.ReadAllText(Name); 23 | public string ReadAllText(Encoding encoding) => StreamParent.ReadAllText(Name, encoding); 24 | public void WriteAllText(string text) => StreamParent.WriteAllText(Name, text); 25 | public void WriteAllText(string text, Encoding encoding) => StreamParent.WriteAllText(Name, text, encoding); 26 | 27 | public void Delete() 28 | { 29 | if (Root != null) 30 | throw new InvalidOperationException("Could not delete the Root Storage!"); 31 | 32 | if (IsStream) 33 | Parent.DeleteStream(Name); 34 | else 35 | Parent.DeleteStorage(Name); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PortableStorage 2 | A Portable Storage for .NET 3 | 4 | ## Features 5 | * Build-in Cashe 6 | * Easy to add provider 7 | * .NET Standard 2.0 File Provider 8 | * Android Storage Access Framework (SAF) File Provider 9 | * ZipStorage provider 10 | 11 | ## nuget 12 | * https://www.nuget.org/packages/PortableStorage/ 13 | * https://www.nuget.org/packages/PortableStorage.Android/ 14 | 15 | ## Usage 16 | 17 | nuget Install-Package PortableStorage 18 | 19 | ### File System Provider 20 | A provider for .NET Standard File. 21 | ```c# 22 | var storage = FileStorgeProvider.CreateStorage("c:/storage", true); 23 | storage.WriteAllText("fileName.txt", "1234"); 24 | ``` 25 | 26 | ### Android Storage Access Framework (SAF) Provider 27 | A provider for Android SAF, easy access to Android external memory, USB OTG and sdcard. 28 | Check Android Sample in the repository! 29 | 30 | 1) First get access to storage Uri by calling SafStorageHelper.BrowserFolder. 31 | 2) Obtain storage object by using the given uri. the uri can also be saved for later usage. 32 | 33 | ```c# 34 | 35 | private const int BROWSE_REQUEST_CODE = 100; //Just a unique number 36 | 37 | 38 | // Select a folder by Intent 39 | private void BrowseOnClick(object sender, EventArgs eventArgs) 40 | { 41 | SafStorageHelper.BrowserFolder(this, BROWSE_REQUEST_CODE); 42 | } 43 | 44 | // Access the folder via SafStorgeProvider 45 | protected override void OnActivityResult(int requestCode, [GeneratedEnum] Result resultCode, Intent data) 46 | { 47 | base.OnActivityResult(requestCode, resultCode, data); 48 | if (requestCode == BROWSE_REQUEST_CODE && resultCode == Result.Ok) 49 | { 50 | var uri = SafStorageHelper.ResolveFromActivityResult(this, data); 51 | var storage = SafStorgeProvider.CreateStorage(this, uri); 52 | storage.CreateStorage("_PortableStorage.Test"); 53 | storage.WriteAllText("test.txt", "123"); 54 | } 55 | } 56 | ``` 57 | 58 | ### Zip Storage Provider (Read-Only) 59 | Provider access to ZipFile same as a storage seamlessly. 60 | 61 | ```c# 62 | var storage = ZipStorgeProvider.CreateStorage("c:/temp/foo.zip"); 63 | var testInZip = storage.ReadAllText("fileName.txt"); 64 | ``` 65 | -------------------------------------------------------------------------------- /PortableStorage.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.28729.10 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PortableStorage", "PortableStorage\PortableStorage.csproj", "{037613F6-867C-4B8E-B487-40DA92BE73BD}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PortableStorage.Android", "PortableStorage.Android\PortableStorage.Android.csproj", "{943961D9-7909-4028-BE74-A699C21DE05F}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AndroidSample", "Samples\AndroidSample\AndroidSample.csproj", "{0B7910FF-1E1E-4A94-BFB9-A3D9039B9E14}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{D1ADEEE0-6EF7-4D35-9620-1B8E99412025}" 13 | EndProject 14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PortableStorage.Test", "PortableStorage.Test\PortableStorage.Test.csproj", "{E3134225-91F5-499D-86CB-16FA311C9C8C}" 15 | EndProject 16 | Global 17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 18 | Debug|Any CPU = Debug|Any CPU 19 | Release|Any CPU = Release|Any CPU 20 | EndGlobalSection 21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 22 | {037613F6-867C-4B8E-B487-40DA92BE73BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {037613F6-867C-4B8E-B487-40DA92BE73BD}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {037613F6-867C-4B8E-B487-40DA92BE73BD}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {037613F6-867C-4B8E-B487-40DA92BE73BD}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {943961D9-7909-4028-BE74-A699C21DE05F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {943961D9-7909-4028-BE74-A699C21DE05F}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {943961D9-7909-4028-BE74-A699C21DE05F}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {943961D9-7909-4028-BE74-A699C21DE05F}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {0B7910FF-1E1E-4A94-BFB9-A3D9039B9E14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {0B7910FF-1E1E-4A94-BFB9-A3D9039B9E14}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {0B7910FF-1E1E-4A94-BFB9-A3D9039B9E14}.Debug|Any CPU.Deploy.0 = Debug|Any CPU 33 | {0B7910FF-1E1E-4A94-BFB9-A3D9039B9E14}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {0B7910FF-1E1E-4A94-BFB9-A3D9039B9E14}.Release|Any CPU.Build.0 = Release|Any CPU 35 | {0B7910FF-1E1E-4A94-BFB9-A3D9039B9E14}.Release|Any CPU.Deploy.0 = Release|Any CPU 36 | {E3134225-91F5-499D-86CB-16FA311C9C8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {E3134225-91F5-499D-86CB-16FA311C9C8C}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {E3134225-91F5-499D-86CB-16FA311C9C8C}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {E3134225-91F5-499D-86CB-16FA311C9C8C}.Release|Any CPU.Build.0 = Release|Any CPU 40 | EndGlobalSection 41 | GlobalSection(SolutionProperties) = preSolution 42 | HideSolutionNode = FALSE 43 | EndGlobalSection 44 | GlobalSection(NestedProjects) = preSolution 45 | {0B7910FF-1E1E-4A94-BFB9-A3D9039B9E14} = {D1ADEEE0-6EF7-4D35-9620-1B8E99412025} 46 | EndGlobalSection 47 | GlobalSection(ExtensibilityGlobals) = postSolution 48 | SolutionGuid = {C1C61240-A36D-4D44-BE10-695E87E4873E} 49 | EndGlobalSection 50 | EndGlobal 51 | -------------------------------------------------------------------------------- /PortableStorage.Test/Resource.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace PortableStorage.Test { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resource { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resource() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PortableStorage.Test.Resource", typeof(Resource).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized resource of type System.Byte[]. 65 | /// 66 | internal static byte[] TestZip { 67 | get { 68 | object obj = ResourceManager.GetObject("TestZip", resourceCulture); 69 | return ((byte[])(obj)); 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /PortableStorage.Android/PortableStorage.Android.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Debug 5 | AnyCPU 6 | 8.0.30703 7 | 2.0 8 | {943961D9-7909-4028-BE74-A699C21DE05F} 9 | {EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 10 | {9ef11e43-1701-4396-8835-8392d57abb70} 11 | Library 12 | Properties 13 | PortableStorage.Droid 14 | PortableStorage.Android 15 | 512 16 | Resources\Resource.designer.cs 17 | Off 18 | false 19 | v11.0 20 | PortableStorage.Android 21 | 1.0.0 22 | PortableStorage.Android 23 | MyCompany 24 | 25 | <IsDevelopmentDependency>False</IsDevelopmentDependency> 26 | <PackageProjectUrl> 27 | </PackageProjectUrl> 28 | <PackageIconUrl /> 29 | <PackageLicenseUrl> 30 | </PackageLicenseUrl> 31 | </PropertyGroup> 32 | <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> 33 | <DebugSymbols>true</DebugSymbols> 34 | <DebugType>portable</DebugType> 35 | <Optimize>false</Optimize> 36 | <OutputPath>bin\Debug\</OutputPath> 37 | <DefineConstants>DEBUG;TRACE</DefineConstants> 38 | <ErrorReport>prompt</ErrorReport> 39 | <WarningLevel>4</WarningLevel> 40 | <LangVersion>8.0</LangVersion> 41 | </PropertyGroup> 42 | <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> 43 | <DebugType>pdbonly</DebugType> 44 | <Optimize>true</Optimize> 45 | <OutputPath>bin\Release\</OutputPath> 46 | <DefineConstants>TRACE</DefineConstants> 47 | <ErrorReport>prompt</ErrorReport> 48 | <WarningLevel>4</WarningLevel> 49 | <LangVersion>8.0</LangVersion> 50 | </PropertyGroup> 51 | <ItemGroup> 52 | <Reference Include="Mono.Android" /> 53 | <Reference Include="System" /> 54 | <Reference Include="System.Core" /> 55 | </ItemGroup> 56 | <ItemGroup> 57 | <None Include="PortableStorage.Android.nuspec" /> 58 | <Compile Include="ChannelStream.cs" /> 59 | <Compile Include="SafStorageHelper.cs" /> 60 | <Compile Include="SafStorgeProvider.cs" /> 61 | <Compile Include="Properties\AssemblyInfo.cs" /> 62 | </ItemGroup> 63 | <ItemGroup> 64 | <ProjectReference Include="..\PortableStorage\PortableStorage.csproj"> 65 | <Project>{037613f6-867c-4b8e-b487-40da92be73bd}</Project> 66 | <Name>PortableStorage</Name> 67 | </ProjectReference> 68 | </ItemGroup> 69 | <Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" /> 70 | <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 71 | Other similar extension points exist, see Microsoft.Common.targets. 72 | <Target Name="BeforeBuild"> 73 | </Target> 74 | <Target Name="AfterBuild"> 75 | </Target> 76 | --> 77 | </Project> -------------------------------------------------------------------------------- /PortableStorage/SyncStream.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace PortableStorage 5 | { 6 | public class SyncStream : Stream 7 | { 8 | private readonly Stream _stream; 9 | private readonly object _lockObject; 10 | private long _position; 11 | 12 | public SyncStream(Stream stream, bool keepCurrentPosition = false) 13 | { 14 | _stream = stream ?? throw new ArgumentNullException(nameof(stream)); 15 | _lockObject = stream; 16 | _position = keepCurrentPosition ? _stream.Position : 0; 17 | } 18 | 19 | public override bool CanRead => _stream.CanRead; 20 | public override bool CanSeek => _stream.CanSeek; 21 | public override bool CanWrite => _stream.CanWrite; 22 | public override long Length 23 | { 24 | get 25 | { 26 | lock (_lockObject) 27 | return _stream.Length; 28 | } 29 | } 30 | 31 | public override void Flush() 32 | { 33 | lock (_lockObject) 34 | _stream.Flush(); 35 | } 36 | public override void SetLength(long value) 37 | { 38 | lock (_lockObject) 39 | _stream.SetLength(value); 40 | } 41 | 42 | public override long Position 43 | { 44 | get 45 | { 46 | lock (_lockObject) 47 | return _position; 48 | } 49 | set 50 | { 51 | lock (_lockObject) 52 | { 53 | if (_position == value) 54 | return; 55 | 56 | // check is seekable 57 | if (!CanSeek) 58 | throw new NotSupportedException(); 59 | 60 | // set next offset 61 | _position = value; 62 | } 63 | } 64 | } 65 | 66 | public override long Seek(long offset, SeekOrigin origin) 67 | { 68 | lock (_lockObject) 69 | { 70 | var newPosition = origin switch 71 | { 72 | SeekOrigin.Begin => offset, 73 | SeekOrigin.Current => offset + _position, 74 | SeekOrigin.End => offset + Length, 75 | _ => throw new NotSupportedException(), 76 | }; 77 | if (_position == newPosition) 78 | return _position; 79 | 80 | // check is seekable 81 | if (!CanSeek) 82 | throw new NotSupportedException(); 83 | 84 | // set next offset 85 | _position = newPosition; 86 | return newPosition; 87 | } 88 | } 89 | 90 | public override int Read(byte[] buffer, int offset, int count) 91 | { 92 | lock (_lockObject) 93 | { 94 | _stream.Position = _position; 95 | var ret = _stream.Read(buffer, offset, count); 96 | _position = _stream.Position; 97 | return ret; 98 | } 99 | } 100 | 101 | public override void Write(byte[] buffer, int offset, int count) 102 | { 103 | lock (_lockObject) 104 | { 105 | _stream.Position = _position; 106 | _stream.Write(buffer, offset, count); 107 | _position = _stream.Position; 108 | } 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /PortableStorage.Android/ChannelStream.cs: -------------------------------------------------------------------------------- 1 | using Android.OS; 2 | using Java.Nio; 3 | using System; 4 | using System.IO; 5 | 6 | namespace PortableStorage.Droid 7 | { 8 | internal class ChannelStream : Stream 9 | { 10 | private Java.Nio.Channels.FileChannel Channel { get; set; } 11 | private readonly string _mode; 12 | private readonly IDisposable _stream; 13 | public ChannelStream(ParcelFileDescriptor parcelFileDescriptor, string mode) 14 | { 15 | _mode = mode; 16 | ParcelFileDescriptor = parcelFileDescriptor; 17 | 18 | if (mode.Contains("w")) 19 | { 20 | var outStream = new Java.IO.FileOutputStream(parcelFileDescriptor.FileDescriptor); 21 | Channel = outStream.Channel; 22 | _stream = outStream; 23 | } 24 | else 25 | { 26 | var inStream = new Java.IO.FileInputStream(parcelFileDescriptor.FileDescriptor); 27 | Channel = inStream.Channel; 28 | _stream = inStream; 29 | } 30 | } 31 | 32 | 33 | private bool _isDisposed = false; 34 | protected override void Dispose(bool disposing) 35 | { 36 | if (!disposing || _isDisposed) 37 | return; 38 | _isDisposed = true; 39 | 40 | (_stream as Java.IO.ICloseable)?.Close(); 41 | _stream?.Dispose(); 42 | 43 | ParcelFileDescriptor?.Close(); 44 | ParcelFileDescriptor?.Dispose(); 45 | 46 | } 47 | 48 | public override bool CanRead => _mode.Contains('r'); 49 | public override bool CanWrite => _mode.Contains('w'); 50 | public override bool CanSeek => !_mode.Contains('a'); 51 | public override long Length => Channel.Size(); 52 | public override long Position 53 | { 54 | get => Channel.Position(); 55 | set => Channel.Position(value); 56 | } 57 | 58 | public ParcelFileDescriptor ParcelFileDescriptor { get; private set; } 59 | 60 | public override void Flush() 61 | { 62 | Channel.Force(false); 63 | } 64 | 65 | public override void SetLength(long value) 66 | { 67 | if (value > Channel.Size()) 68 | { 69 | var orgPosition = Channel.Position(); 70 | Channel.Position(value); 71 | Channel.Position(orgPosition); 72 | } 73 | else 74 | { 75 | Channel.Truncate(value); 76 | } 77 | } 78 | 79 | public override long Seek(long offset, SeekOrigin origin) 80 | { 81 | long pos = offset; 82 | 83 | if (origin == SeekOrigin.Current) 84 | pos = Channel.Position() + offset; 85 | else if (origin == SeekOrigin.End) 86 | pos = Channel.Size() + offset; 87 | 88 | Channel.Position(pos); 89 | return Channel.Position(); 90 | } 91 | 92 | public override int Read(byte[] buffer, int offset, int count) 93 | { 94 | using var buf = ByteBuffer.Allocate(count); 95 | var res = Channel.Read(buf); 96 | buf.Position(0); 97 | 98 | if (buffer.Length == count && offset == 0) 99 | buf.Get(buffer); 100 | else 101 | { 102 | var newBuf = new byte[count]; 103 | buf.Get(newBuf); 104 | Array.Copy(newBuf, 0, buffer, offset, count); 105 | } 106 | 107 | 108 | if (res == -1) return 0; 109 | return res; 110 | } 111 | 112 | public override void Write(byte[] buffer, int offset, int count) 113 | { 114 | byte[] newBuf = buffer; 115 | if (offset != 0 || count != buffer.Length) 116 | { 117 | newBuf = new byte[count]; 118 | Array.Copy(buffer, offset, newBuf, 0, count); 119 | } 120 | 121 | using var buf = ByteBuffer.Wrap(newBuf); 122 | var res = Channel.Write(buf); 123 | } 124 | 125 | } 126 | } -------------------------------------------------------------------------------- /PortableStorage/AesStream.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Security.Cryptography; 4 | 5 | namespace PortableStorage 6 | { 7 | public class AesStream : Stream 8 | { 9 | private readonly Stream _baseStream; 10 | private readonly AesManaged _aes; 11 | private readonly ICryptoTransform _encryptor; 12 | private const int _keySize = 128; 13 | public bool AutoDisposeBaseStream { get; set; } = true; 14 | 15 | /// <param name="salt">//** WARNING **: MUST be unique for each stream otherwise there is NO security</param> 16 | public AesStream(Stream baseStream, string password, byte[] salt) 17 | { 18 | _baseStream = baseStream; 19 | using var key = new Rfc2898DeriveBytes(password, salt, 1000, HashAlgorithmName.SHA256 ); 20 | _aes = new AesManaged 21 | { 22 | KeySize = _keySize, 23 | Mode = CipherMode.ECB, 24 | Padding = PaddingMode.None, 25 | IV = new byte[16], //zero buffer is adequate since we have to use new salt for each stream 26 | Key = key.GetBytes(_keySize / 8) 27 | }; 28 | _encryptor = _aes.CreateEncryptor(_aes.Key, _aes.IV); 29 | } 30 | 31 | private void Cipher(byte[] buffer, int offset, int count, long streamPos) 32 | { 33 | //find block number 34 | var blockSizeInByte = _aes.BlockSize / 8; 35 | var blockNumber = (streamPos / blockSizeInByte) + 1; 36 | var keyPos = streamPos % blockSizeInByte; 37 | 38 | //buffer 39 | var outBuffer = new byte[blockSizeInByte]; 40 | var nonce = new byte[blockSizeInByte]; 41 | var init = false; 42 | 43 | for (int i = offset; i < count; i++) 44 | { 45 | //encrypt the nonce to form next xor buffer (unique key) 46 | if (!init || (keyPos % blockSizeInByte) == 0) 47 | { 48 | BitConverter.GetBytes(blockNumber).CopyTo(nonce, 0); 49 | _encryptor.TransformBlock(nonce, 0, nonce.Length, outBuffer, 0); 50 | if (init) keyPos = 0; 51 | init = true; 52 | blockNumber++; 53 | } 54 | buffer[i] ^= outBuffer[keyPos]; //simple XOR with generated unique key 55 | keyPos++; 56 | } 57 | } 58 | 59 | public override bool CanRead { get { return _baseStream.CanRead; } } 60 | public override bool CanSeek { get { return _baseStream.CanSeek; } } 61 | public override bool CanWrite { get { return _baseStream.CanWrite; } } 62 | public override long Length { get { return _baseStream.Length; } } 63 | public override long Position { get { return _baseStream.Position; } set { _baseStream.Position = value; } } 64 | public override void Flush() { _baseStream.Flush(); } 65 | public override void SetLength(long value) { _baseStream.SetLength(value); } 66 | public override long Seek(long offset, SeekOrigin origin) { return _baseStream.Seek(offset, origin); } 67 | 68 | public override int Read(byte[] buffer, int offset, int count) 69 | { 70 | if (buffer is null) throw new ArgumentNullException(nameof(buffer)); 71 | 72 | var streamPos = Position; 73 | var ret = _baseStream.Read(buffer, offset, count); 74 | Cipher(buffer, offset, count, streamPos); 75 | return ret; 76 | } 77 | 78 | public override void Write(byte[] buffer, int offset, int count) 79 | { 80 | if (buffer is null) throw new ArgumentNullException(nameof(buffer)); 81 | 82 | Cipher(buffer, offset, count, Position); 83 | _baseStream.Write(buffer, offset, count); 84 | } 85 | 86 | protected override void Dispose(bool disposing) 87 | { 88 | if (disposing) 89 | { 90 | _encryptor?.Dispose(); 91 | _aes?.Dispose(); 92 | if (AutoDisposeBaseStream) 93 | _baseStream?.Dispose(); 94 | } 95 | 96 | base.Dispose(disposing); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /PortableStorage.Test/BufferedStreamTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using System; 3 | using System.IO; 4 | using System.Text; 5 | 6 | namespace PortableStorage.Test 7 | { 8 | [TestClass] 9 | public class BufferedStreamTest 10 | { 11 | [TestMethod] 12 | public void SeekAndPosition() 13 | { 14 | using var memStream = new MemoryStream(); 15 | using var bs = new BufferedStream(memStream, 10); 16 | Assert.AreEqual(0, bs.Length); 17 | 18 | var buf = Encoding.ASCII.GetBytes("012345"); 19 | bs.Write(buf); 20 | Assert.AreEqual(0, bs.Seek(0, SeekOrigin.Begin)); 21 | Assert.AreEqual(2, bs.Seek(2, SeekOrigin.Begin)); 22 | Assert.AreEqual(4, bs.Seek(2, SeekOrigin.Current)); 23 | Assert.AreEqual(buf.Length, bs.Seek(0, SeekOrigin.End)); 24 | 25 | var oldLength = buf.Length; 26 | bs.Write(buf, 2, 2); 27 | Assert.AreEqual(oldLength + 2, bs.Position); 28 | Assert.AreEqual(oldLength + 4, bs.Seek(2, SeekOrigin.Current)); 29 | 30 | Assert.AreEqual(15, bs.Seek(15, SeekOrigin.Begin)); 31 | } 32 | 33 | [TestMethod] 34 | public void ReadAndWrite() 35 | { 36 | //write and read 37 | using (var memStream = new MemoryStream()) 38 | using (var bs = new BufferedStream(memStream, 10)) 39 | { 40 | 41 | //less than buffer 42 | var buf1 = Encoding.ASCII.GetBytes("0123456789"); 43 | bs.Write(buf1); 44 | bs.Seek(0, SeekOrigin.Begin); 45 | var buf2 = new byte[buf1.Length]; 46 | bs.Read(buf2, 0, buf2.Length); 47 | Assert.AreEqual(Convert.ToBase64String(buf1), Convert.ToBase64String(buf2)); 48 | 49 | //over buffer 50 | buf1 = Encoding.ASCII.GetBytes("0123456789012345"); 51 | bs.Write(buf1); 52 | bs.Seek(0, SeekOrigin.Begin); 53 | buf2 = new byte[buf1.Length]; 54 | bs.Read(buf2, 0, buf2.Length); 55 | Assert.AreEqual(Convert.ToBase64String(buf1), Convert.ToBase64String(buf2)); 56 | } 57 | 58 | //seek and write 59 | using (var memStream = new MemoryStream()) 60 | using (var bs = new BufferedStream(memStream, 10)) 61 | { 62 | //seek and write 63 | var buf1 = Encoding.ASCII.GetBytes("012345."); 64 | bs.Seek(0, SeekOrigin.End); 65 | bs.Write(buf1); 66 | bs.Seek(0, SeekOrigin.End); 67 | bs.Write(buf1); 68 | bs.Seek(0, SeekOrigin.End); 69 | bs.Write(buf1); 70 | 71 | var buf2 = new byte[buf1.Length * 3]; 72 | bs.Position = 0; 73 | bs.Read(buf2, 0, buf2.Length); 74 | Assert.AreEqual("012345.012345.012345.", Encoding.ASCII.GetString(buf2)); 75 | } 76 | 77 | 78 | 79 | } 80 | 81 | [TestMethod] 82 | public void ReadBuffer() 83 | { 84 | //use read buffer 85 | using (var memStream = new MemoryStream()) 86 | using (var bs = new BufferedStream(memStream, 10)) 87 | { 88 | //seek and write 89 | var buf1 = Encoding.ASCII.GetBytes("123456789.123456789."); 90 | bs.Write(buf1); 91 | 92 | var buf2 = new byte[buf1.Length]; 93 | bs.Position = 0; 94 | bs.Read(buf2, 0, buf2.Length); 95 | Assert.AreEqual("123456789.123456789.", Encoding.ASCII.GetString(buf2)); 96 | 97 | bs.Position = 0; 98 | Array.Clear(buf2, 0, buf2.Length); 99 | bs.Read(buf2, 0, buf2.Length); 100 | Assert.AreEqual("123456789.123456789.", Encoding.ASCII.GetString(buf2)); 101 | 102 | //try using last readed buffer 103 | var buf3 = new byte[10]; 104 | bs.Position = 10; 105 | bs.Read(buf3, 0, buf3.Length); 106 | Assert.AreEqual("123456789.", Encoding.ASCII.GetString(buf3)); 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /PortableStorage/BufferedStream.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace PortableStorage 5 | { 6 | //add the time writing this class; standard BufferedStream was too slow 7 | public class BufferedStream : Stream 8 | { 9 | private readonly Stream _stream; 10 | private readonly int _bufferSize; 11 | private readonly bool _autoDisposeStream; 12 | private readonly byte[] _buf; 13 | private long _bufOffset = 0; 14 | private int _bufPos = 0; 15 | private int _bufUsed = 0; 16 | private bool _isDirty = false; 17 | 18 | public BufferedStream(Stream stream, int bufferSize, bool autoDisposeStream = true) 19 | { 20 | _stream = stream; 21 | _bufferSize = bufferSize; 22 | _autoDisposeStream = autoDisposeStream; 23 | _buf = new byte[bufferSize]; 24 | } 25 | 26 | public override bool CanRead => _stream.CanRead; 27 | public override bool CanSeek => _stream.CanSeek; 28 | public override bool CanWrite => _stream.CanWrite; 29 | 30 | public override long Length 31 | { 32 | get 33 | { 34 | var byBuf = _bufOffset + _bufUsed; 35 | return Math.Max(_stream.Length, byBuf); 36 | } 37 | } 38 | 39 | public override long Position 40 | { 41 | get 42 | { 43 | if (_bufUsed > 0) 44 | return _bufOffset + _bufPos; 45 | return _stream.Position; 46 | } 47 | set => Seek(value, SeekOrigin.Begin); 48 | } 49 | 50 | public override void Flush() 51 | { 52 | FlushDirty(); 53 | _stream.Flush(); 54 | } 55 | 56 | private void FlushDirty() 57 | { 58 | if (_isDirty && _bufUsed > 0) 59 | { 60 | if (_stream.Position != _bufOffset) 61 | _stream.Seek(_bufOffset, SeekOrigin.Begin); 62 | _stream.Write(_buf, 0, _bufUsed); 63 | _bufOffset = Position; 64 | } 65 | _bufPos = 0; 66 | _bufUsed = 0; 67 | Array.Clear(_buf, 0, _bufUsed); 68 | } 69 | 70 | public override int Read(byte[] buffer, int offset, int count) 71 | { 72 | //read already readed 73 | var bufReadCount = Math.Min(count, _bufUsed - _bufPos); 74 | Buffer.BlockCopy(_buf, _bufPos, buffer, offset, bufReadCount); 75 | offset += bufReadCount; 76 | _bufPos += bufReadCount; 77 | var remain = count - bufReadCount; 78 | 79 | if (remain >= _bufferSize) 80 | { 81 | FlushDirty(); 82 | var ret = _stream.Read(buffer, offset, remain) + bufReadCount; 83 | 84 | // fill buffer 85 | _bufOffset = _stream.Position - _bufferSize; 86 | _bufUsed = _bufferSize; 87 | _bufPos = _bufferSize; 88 | Buffer.BlockCopy(buffer, offset, _buf, 0, _bufferSize); 89 | return ret; 90 | } 91 | else if (remain > 0) 92 | { 93 | FlushDirty(); 94 | _bufOffset = _stream.Position; 95 | _bufUsed = _stream.Read(_buf, 0, _bufferSize); //seek 96 | if (_bufUsed > 0) 97 | return Read(buffer, offset, remain) + bufReadCount; 98 | } 99 | 100 | return bufReadCount; 101 | } 102 | 103 | public override void Write(byte[] buffer, int offset, int count) 104 | { 105 | if (count > 0) _isDirty = true; 106 | var bufWriteCount = Math.Min(count, _bufferSize - _bufPos); 107 | Buffer.BlockCopy(buffer, offset, _buf, _bufPos, bufWriteCount); 108 | offset += bufWriteCount; 109 | _bufPos += bufWriteCount; 110 | _bufUsed = Math.Max(_bufUsed, _bufPos); 111 | var remain = count - bufWriteCount; 112 | 113 | if (remain >= _bufferSize) 114 | { 115 | FlushDirty(); 116 | _stream.Write(buffer, offset, remain); 117 | _bufOffset = _stream.Position; 118 | } 119 | else if (remain > 0) 120 | { 121 | FlushDirty(); 122 | Write(buffer, offset, remain); 123 | } 124 | } 125 | 126 | public override long Seek(long offset, SeekOrigin origin) 127 | { 128 | //calcualte required position 129 | var pos = offset; 130 | if (origin == SeekOrigin.Current) pos = Position + offset; 131 | else if (origin == SeekOrigin.End) pos = Length + offset; 132 | 133 | //check is within buffer range 134 | if (pos >= _bufOffset && pos < _bufOffset + _bufferSize) 135 | { 136 | _bufPos = (int)(pos - _bufOffset); 137 | _bufUsed = Math.Max(_bufUsed, _bufPos); 138 | return pos; 139 | } 140 | else 141 | { 142 | FlushDirty(); 143 | if (_stream.Position != offset) 144 | _bufOffset = _stream.Seek(offset, origin); 145 | return _bufOffset; 146 | } 147 | } 148 | 149 | protected override void Dispose(bool disposing) 150 | { 151 | // flush buffer 152 | FlushDirty(); 153 | 154 | //dispose base 155 | base.Dispose(disposing); 156 | 157 | // dispose undelying stream 158 | if (_autoDisposeStream) 159 | _stream.Dispose(); 160 | } 161 | 162 | public override void SetLength(long value) 163 | { 164 | throw new NotImplementedException(); 165 | } 166 | 167 | } 168 | } -------------------------------------------------------------------------------- /PortableStorage.Test/Resource.resx: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <root> 3 | <!-- 4 | Microsoft ResX Schema 5 | 6 | Version 2.0 7 | 8 | The primary goals of this format is to allow a simple XML format 9 | that is mostly human readable. The generation and parsing of the 10 | various data types are done through the TypeConverter classes 11 | associated with the data types. 12 | 13 | Example: 14 | 15 | ... ado.net/XML headers & schema ... 16 | <resheader name="resmimetype">text/microsoft-resx</resheader> 17 | <resheader name="version">2.0</resheader> 18 | <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> 19 | <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> 20 | <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> 21 | <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> 22 | <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> 23 | <value>[base64 mime encoded serialized .NET Framework object]</value> 24 | </data> 25 | <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> 26 | <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> 27 | <comment>This is a comment</comment> 28 | </data> 29 | 30 | There are any number of "resheader" rows that contain simple 31 | name/value pairs. 32 | 33 | Each data row contains a name, and value. The row also contains a 34 | type or mimetype. Type corresponds to a .NET class that support 35 | text/value conversion through the TypeConverter architecture. 36 | Classes that don't support this are serialized and stored with the 37 | mimetype set. 38 | 39 | The mimetype is used for serialized objects, and tells the 40 | ResXResourceReader how to depersist the object. This is currently not 41 | extensible. For a given mimetype the value must be set accordingly: 42 | 43 | Note - application/x-microsoft.net.object.binary.base64 is the format 44 | that the ResXResourceWriter will generate, however the reader can 45 | read any of the formats listed below. 46 | 47 | mimetype: application/x-microsoft.net.object.binary.base64 48 | value : The object must be serialized with 49 | : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter 50 | : and then encoded with base64 encoding. 51 | 52 | mimetype: application/x-microsoft.net.object.soap.base64 53 | value : The object must be serialized with 54 | : System.Runtime.Serialization.Formatters.Soap.SoapFormatter 55 | : and then encoded with base64 encoding. 56 | 57 | mimetype: application/x-microsoft.net.object.bytearray.base64 58 | value : The object must be serialized into a byte array 59 | : using a System.ComponentModel.TypeConverter 60 | : and then encoded with base64 encoding. 61 | --> 62 | <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> 63 | <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> 64 | <xsd:element name="root" msdata:IsDataSet="true"> 65 | <xsd:complexType> 66 | <xsd:choice maxOccurs="unbounded"> 67 | <xsd:element name="metadata"> 68 | <xsd:complexType> 69 | <xsd:sequence> 70 | <xsd:element name="value" type="xsd:string" minOccurs="0" /> 71 | </xsd:sequence> 72 | <xsd:attribute name="name" use="required" type="xsd:string" /> 73 | <xsd:attribute name="type" type="xsd:string" /> 74 | <xsd:attribute name="mimetype" type="xsd:string" /> 75 | <xsd:attribute ref="xml:space" /> 76 | </xsd:complexType> 77 | </xsd:element> 78 | <xsd:element name="assembly"> 79 | <xsd:complexType> 80 | <xsd:attribute name="alias" type="xsd:string" /> 81 | <xsd:attribute name="name" type="xsd:string" /> 82 | </xsd:complexType> 83 | </xsd:element> 84 | <xsd:element name="data"> 85 | <xsd:complexType> 86 | <xsd:sequence> 87 | <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> 88 | <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> 89 | </xsd:sequence> 90 | <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> 91 | <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> 92 | <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> 93 | <xsd:attribute ref="xml:space" /> 94 | </xsd:complexType> 95 | </xsd:element> 96 | <xsd:element name="resheader"> 97 | <xsd:complexType> 98 | <xsd:sequence> 99 | <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> 100 | </xsd:sequence> 101 | <xsd:attribute name="name" type="xsd:string" use="required" /> 102 | </xsd:complexType> 103 | </xsd:element> 104 | </xsd:choice> 105 | </xsd:complexType> 106 | </xsd:element> 107 | </xsd:schema> 108 | <resheader name="resmimetype"> 109 | <value>text/microsoft-resx</value> 110 | </resheader> 111 | <resheader name="version"> 112 | <value>2.0</value> 113 | </resheader> 114 | <resheader name="reader"> 115 | <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> 116 | </resheader> 117 | <resheader name="writer"> 118 | <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> 119 | </resheader> 120 | <assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" /> 121 | <data name="TestZip" type="System.Resources.ResXFileRef, System.Windows.Forms"> 122 | <value>Resources\Test.zip;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> 123 | </data> 124 | </root> -------------------------------------------------------------------------------- /Samples/AndroidSample/MainActivity.cs: -------------------------------------------------------------------------------- 1 | using Android.App; 2 | using Android.Content; 3 | using Android.OS; 4 | using Android.Runtime; 5 | using Android.Widget; 6 | using PortableStorage.Droid; 7 | using System; 8 | using System.IO; 9 | 10 | namespace AndroidSample 11 | { 12 | [Activity(Label = "Portable Storage Sample for SAF", MainLauncher = true, Theme = "@android:style/Theme.Holo.Light")] 13 | public class MainActivity : Activity 14 | { 15 | private const int BROWSE_REQUEST_CODE = 100; 16 | 17 | private Button selectFolderButton; 18 | private Button readWriteButton; 19 | private Button readZipButton; 20 | private TextView infoView; 21 | private TextView uriView; 22 | private void InitUI() 23 | { 24 | var root = new TableLayout(this); 25 | root.SetPadding(0, 20, 0, 0); 26 | 27 | //add buttons 28 | var buttonLayout = new LinearLayout(this); 29 | root.AddView(buttonLayout); 30 | 31 | selectFolderButton = new Button(this) 32 | { 33 | Text = "Select Folder", 34 | }; 35 | selectFolderButton.Click += BrowseOnClick; 36 | buttonLayout.AddView(selectFolderButton); 37 | 38 | readWriteButton = new Button(this) 39 | { 40 | Text = "Read & Write", 41 | }; 42 | readWriteButton.Click += ReadWriteClick; 43 | buttonLayout.AddView(readWriteButton); 44 | 45 | readZipButton = new Button(this) 46 | { 47 | Text = "Read Zip Contents", 48 | }; 49 | readZipButton.Click += ReadZipClick; 50 | buttonLayout.AddView(readZipButton); 51 | 52 | //add uri 53 | uriView = new TextView(this) 54 | { 55 | Text = "Uri: ", 56 | }; 57 | root.AddView(uriView); 58 | 59 | //add text 60 | infoView = new TextView(this) 61 | { 62 | TextSize = 20, 63 | Text = StorageUri == null ? "Info: First \"Select Folder\" then press \"Read & Write\" to check access." : "", 64 | }; 65 | root.AddView(infoView); 66 | SetContentView(root); 67 | } 68 | 69 | protected override void OnCreate(Bundle savedInstanceState) 70 | { 71 | base.OnCreate(savedInstanceState); 72 | Xamarin.Essentials.Platform.Init(this, savedInstanceState); 73 | InitUI(); 74 | 75 | if (StorageUri != null) 76 | uriView.Text = "Uri: " + StorageUri; 77 | } 78 | 79 | public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults) 80 | { 81 | Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults); 82 | 83 | base.OnRequestPermissionsResult(requestCode, permissions, grantResults); 84 | } 85 | 86 | private Uri StorageUri 87 | { 88 | get 89 | { 90 | var value = Xamarin.Essentials.Preferences.Get("LastUri", null); 91 | return value != null ? new Uri(value) : null; 92 | } 93 | set 94 | { 95 | Xamarin.Essentials.Preferences.Set("LastUri", value.ToString()); 96 | uriView.Text = "Uri: " + value; 97 | } 98 | } 99 | 100 | private void BrowseOnClick(object sender, EventArgs eventArgs) 101 | { 102 | SafStorageHelper.BrowserFolder(this, BROWSE_REQUEST_CODE); 103 | } 104 | 105 | private void ReadZipClick(object sender, EventArgs eventArgs) 106 | { 107 | try 108 | { 109 | using var assetStream = Assets.Open("Test.zip"); 110 | using var zipStream = new MemoryStream(); 111 | assetStream.CopyTo(zipStream); //just make it seekable. it doesn't need if it is openned from file 112 | 113 | using var zipStorage = PortableStorage.Providers.ZipStorgeProvider.CreateStorage(zipStream); 114 | var text = zipStorage.ReadAllText("Folder1/File1.txt"); 115 | if (text == "File1 Text.") 116 | { 117 | infoView.Text = "Info: The zip content has been readed successfully :)\n\r"; 118 | } 119 | else 120 | { 121 | throw new Exception("The sample file content couldn't be readed properly!"); 122 | } 123 | } 124 | catch (Exception ex) 125 | { 126 | infoView.Text = "Error: " + ex.Message; 127 | } 128 | } 129 | 130 | private void ReadWriteClick(object sender, EventArgs eventArgs) 131 | { 132 | try 133 | { 134 | if (StorageUri == null) 135 | throw new Exception("No folder has been selected!"); 136 | 137 | var filename = "test.txt"; 138 | var sampleText = "Sample Text"; 139 | using var storage = SafStorgeProvider.CreateStorage(this, StorageUri); 140 | var testStorage = storage.CreateStorage("_PortableStorage.Test"); 141 | testStorage.WriteAllText(filename, sampleText); 142 | var res = testStorage.ReadAllText(filename); 143 | if (res == sampleText) 144 | { 145 | infoView.Text = "Info: The content has been written and readed successfully :)\n\r"; 146 | infoView.Text += "Now you have a access to the storage even after reloading the App."; 147 | } 148 | else 149 | { 150 | throw new Exception("The sample file content couldn't be readed properly!"); 151 | } 152 | } 153 | catch (Exception ex) 154 | { 155 | infoView.Text = "Error: " + ex.Message; 156 | } 157 | } 158 | 159 | protected override void OnActivityResult(int requestCode, [GeneratedEnum] Result resultCode, Intent data) 160 | { 161 | base.OnActivityResult(requestCode, resultCode, data); 162 | 163 | try 164 | { 165 | if (requestCode == BROWSE_REQUEST_CODE && resultCode == Result.Ok) 166 | StorageUri = SafStorageHelper.ResolveFromActivityResult(this, data); 167 | } 168 | catch (Exception ex) 169 | { 170 | infoView.Text = "Error: " + ex.Message; 171 | } 172 | } 173 | } 174 | } 175 | 176 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | # User-specific files 6 | *.suo 7 | *.user 8 | *.userosscache 9 | *.sln.docstates 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | # Build results 13 | [Dd]ebug/ 14 | [Dd]ebugPublic/ 15 | [Rr]elease/ 16 | [Rr]eleases/ 17 | x64/ 18 | x86/ 19 | bld/ 20 | [Bb]in/ 21 | [Oo]bj/ 22 | [Ll]og/ 23 | # Visual Studio 2015/2017 cache/options directory 24 | .vs/ 25 | # Uncomment if you have tasks that create the project's static files in wwwroot 26 | #wwwroot/ 27 | # Visual Studio 2017 auto generated files 28 | Generated\ Files/ 29 | # MSTest test Results 30 | [Tt]est[Rr]esult*/ 31 | [Bb]uild[Ll]og.* 32 | # NUNIT 33 | *.VisualState.xml 34 | TestResult.xml 35 | # Build Results of an ATL Project 36 | [Dd]ebugPS/ 37 | [Rr]eleasePS/ 38 | dlldata.c 39 | # Benchmark Results 40 | BenchmarkDotNet.Artifacts/ 41 | # .NET Core 42 | project.lock.json 43 | project.fragment.lock.json 44 | artifacts/ 45 | **/Properties/launchSettings.json 46 | # StyleCop 47 | StyleCopReport.xml 48 | # Files built by Visual Studio 49 | *_i.c 50 | *_p.c 51 | *_i.h 52 | *.ilk 53 | *.meta 54 | *.obj 55 | *.iobj 56 | *.pch 57 | *.pdb 58 | *.ipdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | # Chutzpah Test files 76 | _Chutzpah* 77 | # Visual C++ cache files 78 | ipch/ 79 | *.aps 80 | *.ncb 81 | *.opendb 82 | *.opensdf 83 | *.sdf 84 | *.cachefile 85 | *.VC.db 86 | *.VC.VC.opendb 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | # Visual Studio Trace Files 93 | *.e2e 94 | # TFS 2012 Local Workspace 95 | $tf/ 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | # ReSharper is a .NET coding add-in 99 | _ReSharper*/ 100 | *.[Rr]e[Ss]harper 101 | *.DotSettings.user 102 | # JustCode is a .NET coding add-in 103 | .JustCode 104 | # TeamCity is a build add-in 105 | _TeamCity* 106 | # DotCover is a Code Coverage Tool 107 | *.dotCover 108 | # AxoCover is a Code Coverage Tool 109 | .axoCover/* 110 | !.axoCover/settings.json 111 | # Visual Studio code coverage results 112 | *.coverage 113 | *.coveragexml 114 | # NCrunch 115 | _NCrunch_* 116 | .*crunch*.local.xml 117 | nCrunchTemp_* 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | # Web workbench (sass) 122 | .sass-cache/ 123 | # Installshield output folder 124 | [Ee]xpress/ 125 | # DocProject is a documentation generator add-in 126 | DocProject/buildhelp/ 127 | DocProject/Help/*.HxT 128 | DocProject/Help/*.HxC 129 | DocProject/Help/*.hhc 130 | DocProject/Help/*.hhk 131 | DocProject/Help/*.hhp 132 | DocProject/Help/Html2 133 | DocProject/Help/html 134 | # Click-Once directory 135 | publish/ 136 | # Publish Web Output 137 | *.[Pp]ublish.xml 138 | *.azurePubxml 139 | # Note: Comment the next line if you want to checkin your web deploy settings, 140 | # but database connection strings (with potential passwords) will be unencrypted 141 | *.pubxml 142 | *.publishproj 143 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 144 | # checkin your Azure Web App publish settings, but sensitive information contained 145 | # in these scripts will be unencrypted 146 | PublishScripts/ 147 | # NuGet Packages 148 | *.nupkg 149 | # The packages folder can be ignored because of Package Restore 150 | **/[Pp]ackages/* 151 | # except build/, which is used as an MSBuild target. 152 | !**/[Pp]ackages/build/ 153 | # Uncomment if necessary however generally it will be regenerated when needed 154 | #!**/[Pp]ackages/repositories.config 155 | # NuGet v3's project.json files produces more ignorable files 156 | *.nuget.props 157 | *.nuget.targets 158 | # Microsoft Azure Build Output 159 | csx/ 160 | *.build.csdef 161 | # Microsoft Azure Emulator 162 | ecf/ 163 | rcf/ 164 | # Windows Store app package directories and files 165 | AppPackages/ 166 | BundleArtifacts/ 167 | Package.StoreAssociation.xml 168 | _pkginfo.txt 169 | *.appx 170 | # Visual Studio cache files 171 | # files ending in .cache can be ignored 172 | *.[Cc]ache 173 | # but keep track of directories ending in .cache 174 | !*.[Cc]ache/ 175 | # Others 176 | ClientBin/ 177 | ~$* 178 | *~ 179 | *.dbmdl 180 | *.dbproj.schemaview 181 | *.jfm 182 | *.pfx 183 | *.publishsettings 184 | orleans.codegen.cs 185 | # Including strong name files can present a security risk 186 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 187 | #*.snk 188 | # Since there are multiple workflows, uncomment next line to ignore bower_components 189 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 190 | #bower_components/ 191 | # RIA/Silverlight projects 192 | Generated_Code/ 193 | # Backup & report files from converting an old project file 194 | # to a newer Visual Studio version. Backup files are not needed, 195 | # because we have git ;-) 196 | _UpgradeReport_Files/ 197 | Backup*/ 198 | UpgradeLog*.XML 199 | UpgradeLog*.htm 200 | ServiceFabricBackup/ 201 | *.rptproj.bak 202 | # SQL Server files 203 | *.mdf 204 | *.ldf 205 | *.ndf 206 | # Business Intelligence projects 207 | *.rdl.data 208 | *.bim.layout 209 | *.bim_*.settings 210 | *.rptproj.rsuser 211 | # Microsoft Fakes 212 | FakesAssemblies/ 213 | # GhostDoc plugin setting file 214 | *.GhostDoc.xml 215 | # Node.js Tools for Visual Studio 216 | .ntvs_analysis.dat 217 | node_modules/ 218 | # Visual Studio 6 build log 219 | *.plg 220 | # Visual Studio 6 workspace options file 221 | *.opt 222 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 223 | *.vbw 224 | # Visual Studio LightSwitch build output 225 | **/*.HTMLClient/GeneratedArtifacts 226 | **/*.DesktopClient/GeneratedArtifacts 227 | **/*.DesktopClient/ModelManifest.xml 228 | **/*.Server/GeneratedArtifacts 229 | **/*.Server/ModelManifest.xml 230 | _Pvt_Extensions 231 | # Paket dependency manager 232 | .paket/paket.exe 233 | paket-files/ 234 | # FAKE - F# Make 235 | .fake/ 236 | # JetBrains Rider 237 | .idea/ 238 | *.sln.iml 239 | # CodeRush 240 | .cr/ 241 | # Python Tools for Visual Studio (PTVS) 242 | __pycache__/ 243 | *.pyc 244 | # Cake - Uncomment if you are using it 245 | # tools/** 246 | # !tools/packages.config 247 | # Tabs Studio 248 | *.tss 249 | # Telerik's JustMock configuration file 250 | *.jmconfig 251 | # BizTalk build output 252 | *.btp.cs 253 | *.btm.cs 254 | *.odx.cs 255 | *.xsd.cs 256 | # OpenCover UI analysis results 257 | OpenCover/ 258 | # Azure Stream Analytics local run output 259 | ASALocalRun/ 260 | # MSBuild Binary and Structured Log 261 | *.binlog 262 | # NVidia Nsight GPU debugger configuration file 263 | *.nvuser 264 | # MFractors (Xamarin productivity tool) working folder 265 | .mfractor/ 266 | 267 | .nuget 268 | -------------------------------------------------------------------------------- /Samples/AndroidSample/AndroidSample.csproj: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> 3 | <PropertyGroup> 4 | <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> 5 | <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> 6 | <ProductVersion>8.0.30703</ProductVersion> 7 | <SchemaVersion>2.0</SchemaVersion> 8 | <ProjectGuid>{0B7910FF-1E1E-4A94-BFB9-A3D9039B9E14}</ProjectGuid> 9 | <ProjectTypeGuids>{EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids> 10 | <TemplateGuid>{84dd83c5-0fe3-4294-9419-09e7c8ba324f}</TemplateGuid> 11 | <OutputType>Library</OutputType> 12 | <AppDesignerFolder>Properties</AppDesignerFolder> 13 | <RootNamespace>AndroidSample</RootNamespace> 14 | <AssemblyName>AndroidSample</AssemblyName> 15 | <FileAlignment>512</FileAlignment> 16 | <AndroidApplication>True</AndroidApplication> 17 | <AndroidResgenFile>Resources\Resource.designer.cs</AndroidResgenFile> 18 | <AndroidResgenClass>Resource</AndroidResgenClass> 19 | <GenerateSerializationAssemblies>Off</GenerateSerializationAssemblies> 20 | <AndroidUseLatestPlatformSdk>false</AndroidUseLatestPlatformSdk> 21 | <TargetFrameworkVersion>v11.0</TargetFrameworkVersion> 22 | <AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest> 23 | <MonoAndroidResourcePrefix>Resources</MonoAndroidResourcePrefix> 24 | <MonoAndroidAssetsPrefix>Assets</MonoAndroidAssetsPrefix> 25 | <AndroidEnableSGenConcurrent>true</AndroidEnableSGenConcurrent> 26 | <AndroidHttpClientHandlerType>Xamarin.Android.Net.AndroidClientHandler</AndroidHttpClientHandlerType> 27 | </PropertyGroup> 28 | <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> 29 | <DebugSymbols>True</DebugSymbols> 30 | <DebugType>portable</DebugType> 31 | <Optimize>False</Optimize> 32 | <OutputPath>bin\Debug\</OutputPath> 33 | <DefineConstants>DEBUG;TRACE</DefineConstants> 34 | <ErrorReport>prompt</ErrorReport> 35 | <WarningLevel>4</WarningLevel> 36 | <AndroidUseSharedRuntime>True</AndroidUseSharedRuntime> 37 | <AndroidLinkMode>None</AndroidLinkMode> 38 | <EmbedAssembliesIntoApk>False</EmbedAssembliesIntoApk> 39 | <AotAssemblies>false</AotAssemblies> 40 | <EnableLLVM>false</EnableLLVM> 41 | <BundleAssemblies>false</BundleAssemblies> 42 | <MandroidI18n /> 43 | <AndroidEnableProfiledAot>false</AndroidEnableProfiledAot> 44 | <LangVersion>8.0</LangVersion> 45 | </PropertyGroup> 46 | <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> 47 | <DebugType>PdbOnly</DebugType> 48 | <DebugSymbols>True</DebugSymbols> 49 | <Optimize>True</Optimize> 50 | <OutputPath>bin\Release\</OutputPath> 51 | <DefineConstants>TRACE</DefineConstants> 52 | <ErrorReport>prompt</ErrorReport> 53 | <WarningLevel>4</WarningLevel> 54 | <AndroidManagedSymbols>true</AndroidManagedSymbols> 55 | <AndroidUseSharedRuntime>False</AndroidUseSharedRuntime> 56 | <AndroidLinkMode>SdkOnly</AndroidLinkMode> 57 | <EmbedAssembliesIntoApk>True</EmbedAssembliesIntoApk> 58 | <AotAssemblies>false</AotAssemblies> 59 | <EnableLLVM>false</EnableLLVM> 60 | <BundleAssemblies>false</BundleAssemblies> 61 | <LangVersion>8.0</LangVersion> 62 | </PropertyGroup> 63 | <ItemGroup> 64 | <Reference Include="System" /> 65 | <Reference Include="System.Core" /> 66 | <Reference Include="Mono.Android" /> 67 | <Reference Include="System.IO.Compression" /> 68 | <Reference Include="System.Web.Services" /> 69 | </ItemGroup> 70 | <ItemGroup> 71 | <Compile Include="MainActivity.cs" /> 72 | <Compile Include="Properties\AssemblyInfo.cs" /> 73 | </ItemGroup> 74 | <ItemGroup> 75 | <AndroidAsset Include="Assets\Test.zip" /> 76 | <None Include="Resources\AboutResources.txt" /> 77 | <None Include="Properties\AndroidManifest.xml" /> 78 | <None Include="Assets\AboutAssets.txt" /> 79 | </ItemGroup> 80 | <ItemGroup> 81 | <AndroidResource Include="Resources\values\colors.xml" /> 82 | <AndroidResource Include="Resources\values\dimens.xml" /> 83 | <AndroidResource Include="Resources\values\ic_launcher_background.xml" /> 84 | <AndroidResource Include="Resources\values\strings.xml" /> 85 | <AndroidResource Include="Resources\values\styles.xml" /> 86 | <AndroidResource Include="Resources\mipmap-anydpi-v26\ic_launcher.xml" /> 87 | <AndroidResource Include="Resources\mipmap-anydpi-v26\ic_launcher_round.xml" /> 88 | <AndroidResource Include="Resources\mipmap-hdpi\ic_launcher.png" /> 89 | <AndroidResource Include="Resources\mipmap-hdpi\ic_launcher_foreground.png" /> 90 | <AndroidResource Include="Resources\mipmap-hdpi\ic_launcher_round.png" /> 91 | <AndroidResource Include="Resources\mipmap-mdpi\ic_launcher.png" /> 92 | <AndroidResource Include="Resources\mipmap-mdpi\ic_launcher_foreground.png" /> 93 | <AndroidResource Include="Resources\mipmap-mdpi\ic_launcher_round.png" /> 94 | <AndroidResource Include="Resources\mipmap-xhdpi\ic_launcher.png" /> 95 | <AndroidResource Include="Resources\mipmap-xhdpi\ic_launcher_foreground.png" /> 96 | <AndroidResource Include="Resources\mipmap-xhdpi\ic_launcher_round.png" /> 97 | <AndroidResource Include="Resources\mipmap-xxhdpi\ic_launcher.png" /> 98 | <AndroidResource Include="Resources\mipmap-xxhdpi\ic_launcher_foreground.png" /> 99 | <AndroidResource Include="Resources\mipmap-xxhdpi\ic_launcher_round.png" /> 100 | <AndroidResource Include="Resources\mipmap-xxxhdpi\ic_launcher.png" /> 101 | <AndroidResource Include="Resources\mipmap-xxxhdpi\ic_launcher_foreground.png" /> 102 | <AndroidResource Include="Resources\mipmap-xxxhdpi\ic_launcher_round.png" /> 103 | </ItemGroup> 104 | <ItemGroup> 105 | <PackageReference Include="Xamarin.Essentials" Version="1.6.1" /> 106 | </ItemGroup> 107 | <ItemGroup> 108 | <ProjectReference Include="..\..\PortableStorage.Android\PortableStorage.Android.csproj"> 109 | <Project>{943961d9-7909-4028-be74-a699c21de05f}</Project> 110 | <Name>PortableStorage.Android</Name> 111 | </ProjectReference> 112 | <ProjectReference Include="..\..\PortableStorage\PortableStorage.csproj"> 113 | <Project>{037613f6-867c-4b8e-b487-40da92be73bd}</Project> 114 | <Name>PortableStorage</Name> 115 | </ProjectReference> 116 | </ItemGroup> 117 | <ItemGroup> 118 | <Folder Include="Resources\layout\" /> 119 | <Folder Include="Resources\menu\" /> 120 | </ItemGroup> 121 | <Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" /> 122 | <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 123 | Other similar extension points exist, see Microsoft.Common.targets. 124 | <Target Name="BeforeBuild"> 125 | </Target> 126 | <Target Name="AfterBuild"> 127 | </Target> 128 | --> 129 | </Project> -------------------------------------------------------------------------------- /PortableStorage.Test/ZipStorageTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using PortableStorage.Providers; 3 | using System; 4 | using System.IO; 5 | using System.IO.Compression; 6 | 7 | namespace PortableStorage.Test 8 | { 9 | [TestClass] 10 | public class ZipStorageTest 11 | { 12 | private static string TempPath => Path.Combine(Path.GetTempPath(), "_test_portablestroage_zip"); 13 | 14 | private StorageRoot GetTempStorage(bool useCache = true) 15 | { 16 | var options = new StorageOptions 17 | { 18 | CacheTimeout = useCache ? -1 : 0 19 | }; 20 | 21 | var tempPath = Path.Combine(TempPath, Guid.NewGuid().ToString()); 22 | var storage = FileStorgeProvider.CreateStorage(tempPath, true, options); 23 | return storage; 24 | 25 | } 26 | 27 | private Stream GetTempZipStream() 28 | { 29 | var buf = new byte[10000]; 30 | 31 | using (var zipArchive = new ZipArchive(new MemoryStream(buf), ZipArchiveMode.Create)) 32 | { 33 | AddToZipArchive(zipArchive, "file1.txt", "file1.txt contents."); 34 | AddToZipArchive(zipArchive, "/file4.txt", "file4.txt contents."); 35 | AddToZipArchive(zipArchive, "folder1/file2.txt", "file2.txt contents."); 36 | AddToZipArchive(zipArchive, "\\folder_backslash\\file.txt", "file.txt contents."); 37 | AddToZipArchive(zipArchive, "folder1/folder2/file3.txt", "file3.txt contents."); 38 | } 39 | 40 | return new MemoryStream(buf); 41 | } 42 | 43 | 44 | [TestMethod] 45 | public void Open_zip_storage_and_stream_by_provider() 46 | { 47 | using var zipStream = GetTempZipStream(); 48 | using var zipStorage = ZipStorgeProvider.CreateStorage(zipStream); 49 | Assert.IsTrue(zipStorage.StreamExists("file1.txt")); 50 | Assert.IsTrue(zipStorage.StreamExists("file4.txt")); 51 | Assert.IsTrue(zipStorage.StreamExists("folder1/file2.txt")); 52 | Assert.IsTrue(zipStorage.StreamExists("folder_backslash/file.txt")); 53 | Assert.IsTrue(zipStorage.StorageExists("folder1/folder2")); 54 | Assert.IsFalse(zipStorage.StorageExists("folder1/folder2/file3.txt")); 55 | 56 | var str = zipStorage.OpenStorage("folder1").OpenStorage("folder2").ReadAllText("file3.txt"); 57 | Assert.AreEqual("file3.txt contents.", str, "unexpected text has been readed"); 58 | } 59 | 60 | private static void AddToZipArchive(ZipArchive zipArchive, string path, string text) 61 | { 62 | //css/css.txt 63 | using var stream = zipArchive.CreateEntry(path).Open(); 64 | using var writer = new StreamWriter(stream); 65 | writer.Write(text); 66 | } 67 | 68 | [TestMethod] 69 | public void Open_zip_storage_and_stream_by_ZipArchive_without_directory_entry() 70 | { 71 | var buf = new byte[10000]; 72 | using (var zipArchive = new ZipArchive(new MemoryStream(buf), ZipArchiveMode.Create)) 73 | { 74 | AddToZipArchive(zipArchive, "folder1/folder1/folder1/file1.zip", "z"); 75 | AddToZipArchive(zipArchive, "folder1/folder1/folder1/file2.zip", "z"); 76 | } 77 | 78 | using var zipStream = new MemoryStream(buf); 79 | var zipStorage = ZipStorgeProvider.CreateStorage(zipStream); 80 | Assert.IsTrue(zipStorage.StorageExists("folder1")); 81 | Assert.IsTrue(zipStorage.StorageExists("folder1/folder1")); 82 | Assert.IsTrue(zipStorage.StorageExists("folder1/folder1")); 83 | Assert.IsTrue(zipStorage.StorageExists("folder1/folder1/folder1")); 84 | Assert.IsTrue(zipStorage.StreamExists("folder1/folder1/folder1/file1.zip")); 85 | Assert.IsTrue(zipStorage.StreamExists("folder1/folder1/folder1/file2.zip")); 86 | 87 | var str = zipStorage.ReadAllText("folder1/folder1/folder1/file2.zip"); 88 | Assert.AreEqual("z", str, "unexpected text has been readed"); 89 | } 90 | 91 | [TestMethod] 92 | public void Open_zip_storage_and_stream_by_virtualprovider() 93 | { 94 | using var storage = GetTempStorage(); 95 | var folder1 = storage.CreateStorage("folder1"); 96 | 97 | using (var zipStreamSrc = GetTempZipStream()) 98 | using (var zipStreamDest = folder1.CreateStream("test.zip")) 99 | zipStreamSrc.CopyTo(zipStreamDest); 100 | 101 | Assert.IsTrue(storage.StreamExists("folder1/test.zip")); 102 | Assert.IsTrue(storage.StorageExists("folder1/test.zip/folder1")); 103 | Assert.IsTrue(storage.StreamExists("folder1/test.zip/folder1/file2.txt")); 104 | Assert.AreEqual("file3.txt contents.", storage.ReadAllText("folder1/test.zip/folder1/folder2/file3.txt"), "unexpected text has been readed"); 105 | 106 | // check IsVirtual 107 | Assert.IsFalse(storage.OpenStorage("folder1").IsVirtual); 108 | Assert.IsTrue(storage.OpenStorage("folder1/test.zip").IsVirtual); 109 | Assert.IsTrue(storage.OpenStorage("folder1/test.zip/folder1").IsVirtual); 110 | Assert.IsTrue(storage.OpenStorage("folder1/test.zip/folder1/folder2").IsVirtual); 111 | } 112 | 113 | [TestMethod] 114 | public void Map_virtualStorage__after_Rename() 115 | { 116 | using var storage = GetTempStorage(); 117 | var folder1 = storage.CreateStorage("folder1"); 118 | 119 | using (var zipStreamSrc = GetTempZipStream()) 120 | using (var zipStreamDest = folder1.CreateStream("test.zz")) 121 | zipStreamSrc.CopyTo(zipStreamDest); 122 | 123 | Assert.IsFalse(storage.GetEntry("folder1/test.zz").IsVirtualStorage); 124 | Assert.IsFalse(storage.GetEntry("folder1/test.zz").IsStorage); 125 | 126 | //rename to zip 127 | storage.Rename("folder1/test.zz", "test.zip"); 128 | Assert.IsTrue(storage.GetEntry("folder1/test.zip").IsVirtualStorage, "Virtual Storage doesn't map!"); 129 | Assert.IsTrue(storage.GetEntry("folder1/test.zip").IsStorage, "Virtual Storage doesn't map!"); 130 | } 131 | 132 | 133 | [TestMethod] 134 | public void Open_zip_storage_by_resource() 135 | { 136 | using var zipStream = new MemoryStream(Resource.TestZip); 137 | using var zipStorage = ZipStorgeProvider.CreateStorage(zipStream); 138 | var text = zipStorage.ReadAllText("Folder1/File1.txt"); 139 | Assert.AreEqual("File1 Text.", text, "The sample file content couldn't be readed properly!"); 140 | Assert.IsTrue(zipStorage.StreamExists("Root.txt")); 141 | Assert.IsTrue(zipStorage.StorageExists("Folder1")); 142 | } 143 | 144 | 145 | [TestMethod] 146 | public void Dispose_zip_storage_by_virtual_folder() 147 | { 148 | using var storage = GetTempStorage(); 149 | var folder1 = storage.CreateStorage("folder1"); 150 | using (var zipStreamSrc = GetTempZipStream()) 151 | using (var zipStreamDest = folder1.CreateStream("test.zip")) 152 | zipStreamSrc.CopyTo(zipStreamDest); 153 | 154 | var stream = storage.OpenStreamRead("folder1/test.zip/folder1/folder2/file3.txt"); 155 | stream.Dispose(); 156 | 157 | Assert.IsTrue(storage.StreamExists("folder1/test.zip")); 158 | storage.DeleteStream("folder1/test.zip"); 159 | } 160 | 161 | [TestMethod] 162 | public void Deleted_and_Rename_Mapped_VirtualFolder() 163 | { 164 | using var storage = GetTempStorage(); 165 | var folder1 = storage.CreateStorage("folder1"); 166 | using (var zipStreamSrc = GetTempZipStream()) 167 | using (var zipStreamDest = folder1.CreateStream("test.zip")) 168 | zipStreamSrc.CopyTo(zipStreamDest); 169 | 170 | var stream = storage.OpenStreamRead("folder1/test.zip/folder1/folder2/file3.txt"); 171 | 172 | //try delete the zip file while another stream is open 173 | storage.Rename("folder1/test.zip", "test2.zip"); 174 | 175 | var stream2 = storage.OpenStreamRead("folder1/test2.zip/folder1/folder2/file3.txt"); 176 | storage.DeleteStorage("folder1"); 177 | } 178 | 179 | 180 | [ClassCleanup] 181 | public static void ClassCleanup() 182 | { 183 | if (Directory.Exists(TempPath)) 184 | Directory.Delete(TempPath, true); 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /PortableStorage/Providers/FileStorgeProvider.cs: -------------------------------------------------------------------------------- 1 | using PortableStorage.Exceptions; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | 6 | namespace PortableStorage.Providers 7 | { 8 | public class FileStorgeProvider : IStorageProvider 9 | { 10 | public Uri Uri => new Uri(PathUtil.AddLastSeparator(SystemPath)); 11 | public string SystemPath { get; private set; } 12 | public bool IsGetEntriesBySearchPatternFast => true; 13 | public bool IsGetEntryUriByNameFast => true; 14 | 15 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "<Pending>")] 16 | public static StorageRoot CreateStorage(string path, bool createIfNotExists, StorageOptions storageOptions = null) 17 | { 18 | var provider = new FileStorgeProvider(path, createIfNotExists); 19 | var ret = new StorageRoot(provider, storageOptions); 20 | return ret; 21 | } 22 | 23 | public FileStorgeProvider(string path, bool createIfNotExists = false) 24 | { 25 | SystemPath = PathUtil.AddLastSeparator(path); 26 | 27 | //check existance 28 | if (!Directory.Exists(path)) 29 | { 30 | if (createIfNotExists) 31 | Directory.CreateDirectory(path); 32 | else 33 | throw new StorageNotFoundException(PathToUri(path)); 34 | } 35 | } 36 | 37 | public string Name => Path.GetFileName(PathUtil.RemoveLastSeparator(SystemPath)); 38 | 39 | public long GetFreeSpace() 40 | { 41 | var drive = new DriveInfo(SystemPath); 42 | var driveInfo = new DriveInfo(drive.Name); 43 | var ret = driveInfo.AvailableFreeSpace; 44 | return ret; 45 | } 46 | 47 | public CreateStorageResult CreateStorage(string name) 48 | { 49 | var folderPath = Path.Combine(SystemPath, name); 50 | Directory.CreateDirectory(folderPath); 51 | var ret = new CreateStorageResult() 52 | { 53 | Entry = StorageProviderEntryFromPath(folderPath), 54 | Storage = new FileStorgeProvider(folderPath) 55 | }; 56 | return ret; 57 | } 58 | 59 | public CreateStreamResult CreateStream(string name, StreamAccess access, StreamShare share, int bufferSize) 60 | { 61 | var filePath = Path.Combine(SystemPath, name); 62 | var fs = OpenStream(filePath, FileMode.Create, access, share, bufferSize); 63 | var ret = new CreateStreamResult() 64 | { 65 | EntryBase = StorageProviderEntryFromPath(filePath), 66 | Stream = fs 67 | }; 68 | return ret; 69 | } 70 | 71 | public IStorageProvider OpenStorage(Uri uri) 72 | { 73 | var folderPath = PathFromUri(uri); 74 | if (!Directory.Exists(folderPath)) 75 | throw new StorageNotFoundException(new Uri(folderPath)); 76 | var ret = new FileStorgeProvider(folderPath); 77 | return ret; 78 | } 79 | 80 | public StorageEntryBase[] GetEntries(string searchPattern) 81 | { 82 | var fsEntries = string.IsNullOrEmpty(searchPattern) ? Directory.GetFileSystemEntries(SystemPath) : Directory.GetFileSystemEntries(SystemPath, searchPattern); 83 | 84 | var ret = new List<StorageEntryBase>(fsEntries.Length); 85 | foreach (var fsEntry in fsEntries) 86 | { 87 | var entry = StorageProviderEntryFromPath(fsEntry); 88 | ret.Add(entry); 89 | } 90 | return ret.ToArray(); 91 | } 92 | 93 | public Uri Rename(Uri uri, string desName) 94 | { 95 | var srcPath = PathFromUri(uri); 96 | var desPath = Path.Combine(SystemPath, desName); 97 | 98 | var attr = File.GetAttributes(srcPath); 99 | if (attr.HasFlag(FileAttributes.Directory)) 100 | Directory.Move(srcPath, desPath); 101 | else 102 | File.Move(srcPath, desPath); 103 | 104 | return PathToUri(desPath); 105 | } 106 | 107 | public Stream OpenStream(Uri uri, StreamMode mode, StreamAccess access, StreamShare share, int bufferSize) 108 | { 109 | var fmode = FileMode.Append; 110 | switch (mode) 111 | { 112 | case StreamMode.Append: fmode = FileMode.Append; break; 113 | case StreamMode.Open: fmode = FileMode.Open; break; 114 | case StreamMode.Truncate: fmode = FileMode.Truncate; break; 115 | } 116 | 117 | var filePath = PathFromUri(uri); 118 | return OpenStream(filePath, fmode, access, share, bufferSize); 119 | } 120 | 121 | private Stream OpenStream(string filePath, FileMode fmode, StreamAccess access, StreamShare share, int _) 122 | { 123 | var faccess = FileAccess.Read; 124 | switch (access) 125 | { 126 | case StreamAccess.Read: faccess = FileAccess.Read; break; 127 | case StreamAccess.ReadWrite: faccess = FileAccess.ReadWrite; break; 128 | case StreamAccess.Write: faccess = FileAccess.Write; break; 129 | } 130 | 131 | FileShare fshare = FileShare.None; 132 | switch (share) 133 | { 134 | case StreamShare.None: fshare = FileShare.None; break; 135 | case StreamShare.Read: fshare = FileShare.Read; break; 136 | case StreamShare.ReadWrite: fshare = FileShare.ReadWrite; break; 137 | case StreamShare.Write: fshare = FileShare.Write; break; 138 | } 139 | 140 | return File.Open(filePath, fmode, faccess, fshare); 141 | } 142 | 143 | public void RemoveStream(Uri uri) 144 | { 145 | var filePath = PathFromUri(uri); 146 | File.Delete(filePath); 147 | } 148 | 149 | public void RemoveStorage(Uri uri) 150 | { 151 | var folderPath = PathFromUri(uri); 152 | Directory.Delete(folderPath, true); 153 | } 154 | 155 | public void SetAttributes(Uri uri, StreamAttributes attributes) 156 | { 157 | FileAttributes fattr = 0; 158 | var filePath = PathFromUri(uri); 159 | if (attributes.HasFlag(StreamAttributes.Hidden)) fattr |= FileAttributes.Hidden; 160 | if (attributes.HasFlag(StreamAttributes.System)) fattr |= FileAttributes.System; 161 | File.SetAttributes(filePath, fattr); 162 | } 163 | 164 | 165 | public StreamAttributes GetAttributes(Uri uri) 166 | { 167 | StreamAttributes attr = 0; 168 | 169 | var filePath = PathFromUri(uri); 170 | var fileAttr = File.GetAttributes(filePath); 171 | if (fileAttr.HasFlag(FileAttributes.Hidden)) attr |= StreamAttributes.Hidden; 172 | if (fileAttr.HasFlag(FileAttributes.System)) attr |= StreamAttributes.System; 173 | 174 | return attr; 175 | } 176 | 177 | private string PathFromUri(Uri uri) 178 | { 179 | if (uri == null) throw new ArgumentNullException(nameof(uri)); 180 | 181 | var name = Path.GetFileName(uri.LocalPath); 182 | return Path.Combine(SystemPath, name); 183 | } 184 | 185 | private static Uri PathToUri(string path) 186 | { 187 | return new Uri(path); 188 | } 189 | 190 | private static StorageEntryBase StorageProviderEntryFromPath(string path) 191 | { 192 | return StorageProviderEntryFromFileInfo(new FileInfo(path)); 193 | } 194 | 195 | private static StorageEntryBase StorageProviderEntryFromFileInfo(FileInfo fileInfo) 196 | { 197 | StreamAttributes attr = 0; 198 | var fileAttr = fileInfo.Attributes; 199 | if (fileAttr.HasFlag(FileAttributes.Hidden)) attr |= StreamAttributes.Hidden; 200 | if (fileAttr.HasFlag(FileAttributes.System)) attr |= StreamAttributes.System; 201 | 202 | var ret = new StorageEntryBase() 203 | { 204 | Uri = new Uri(fileInfo.FullName), 205 | Attributes = attr, 206 | IsStorage = fileAttr.HasFlag(FileAttributes.Directory), 207 | Name = fileInfo.Name, 208 | LastWriteTime = fileInfo.LastWriteTime, 209 | Size = fileAttr.HasFlag(FileAttributes.Directory) ? 0 : fileInfo.Length 210 | }; 211 | 212 | return ret; 213 | } 214 | 215 | private bool _disposedValue = false; // To detect redundant calls 216 | protected virtual void Dispose(bool disposing) 217 | { 218 | if (_disposedValue) 219 | return; 220 | 221 | if (disposing) 222 | { 223 | // dispose managed state (managed objects). 224 | } 225 | 226 | // free unmanaged resources (unmanaged objects) and override a finalizer below. 227 | // set large fields to null. 228 | 229 | _disposedValue = true; 230 | } 231 | 232 | public void Dispose() 233 | { 234 | Dispose(true); 235 | } 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /PortableStorage/Providers/ZipStorgeProvider.cs: -------------------------------------------------------------------------------- 1 | using PortableStorage.Exceptions; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.IO.Compression; 6 | using System.Linq; 7 | using System.Web; 8 | 9 | namespace PortableStorage.Providers 10 | { 11 | public class ZipStorgeProvider : IStorageProvider 12 | { 13 | public Uri Uri => PathToUri(_path); 14 | public bool IsGetEntriesBySearchPatternFast => false; 15 | public bool IsGetEntryUriByNameFast => true; 16 | 17 | public static StorageRoot CreateStorage(string zipPath, StorageOptions storageOptions = null) 18 | { 19 | var provider = new ZipStorgeProvider(zipPath); 20 | var ret = new StorageRoot(provider, storageOptions); 21 | return ret; 22 | } 23 | 24 | public static StorageRoot CreateStorage(Stream stream, Uri streamUri = null, string streamName = null, StorageOptions storageOptions = null) 25 | { 26 | var provider = new ZipStorgeProvider(stream, streamUri, streamName); 27 | var ret = new StorageRoot(provider, storageOptions); 28 | return ret; 29 | } 30 | 31 | private readonly string _path = "/"; 32 | private readonly ZipArchive _zipArchive; 33 | private readonly string _name; 34 | private readonly Uri _streamUri; 35 | 36 | public ZipStorgeProvider(string zipPath) 37 | : this(File.OpenRead(zipPath), new Uri(zipPath), Path.GetFileName(zipPath)) 38 | { 39 | 40 | } 41 | 42 | public ZipStorgeProvider(Stream stream, Uri streamUri = null, string streamName = null, bool leaveStreamOpen = false) 43 | { 44 | //var syncStream = new SyncStream(stream); 45 | _zipArchive = new ZipArchive(stream, ZipArchiveMode.Read, leaveStreamOpen); 46 | _streamUri = streamUri; 47 | _name = streamName; 48 | } 49 | 50 | private ZipStorgeProvider(ZipStorgeProvider parent, string path) 51 | { 52 | _path = path; 53 | _zipArchive = parent._zipArchive; 54 | } 55 | 56 | public string Name => _name ?? Path.GetFileName(PathUtil.RemoveLastSeparator(_path)); 57 | 58 | public long GetFreeSpace() 59 | { 60 | return 0; 61 | } 62 | 63 | public CreateStorageResult CreateStorage(string name) => throw new NotSupportedException(); 64 | public CreateStreamResult CreateStream(string name, StreamAccess access, StreamShare share, int bufferSize) => throw new NotSupportedException(); 65 | public Uri Rename(Uri uri, string desName) => throw new NotSupportedException(); 66 | public void RemoveStream(Uri uri) => throw new NotSupportedException(); 67 | public void RemoveStorage(Uri uri) => throw new NotSupportedException(); 68 | public void SetAttributes(Uri uri, StreamAttributes attributes) => throw new NotSupportedException(); 69 | 70 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "<Pending>")] 71 | public StreamAttributes GetAttributes(Uri uri) 72 | { 73 | if (uri == null) 74 | throw new ArgumentNullException(nameof(uri)); 75 | 76 | StreamAttributes attr = 0; 77 | return attr; 78 | } 79 | 80 | public IStorageProvider OpenStorage(Uri uri) 81 | { 82 | if (uri == null) throw new ArgumentNullException(nameof(uri)); 83 | 84 | lock (_zipArchive) 85 | { 86 | var path = PathFromUri(uri); 87 | var exists = _zipArchive.Entries.Any(x => GetEntryFolderName(x.FullName).IndexOf(path, StringComparison.OrdinalIgnoreCase) == 0); 88 | if (!exists) 89 | throw new StorageNotFoundException(uri); 90 | 91 | var ret = new ZipStorgeProvider(this, path); 92 | return ret; 93 | } 94 | } 95 | 96 | private static string GetEntryFolderName(string fullName) 97 | { 98 | return GetEntryFolderName(fullName, out _); 99 | } 100 | 101 | private static string GetEntryFolderName(string fullName, out string fixFullName) 102 | { 103 | //fix and add first separator to fullname 104 | fullName = fullName.Replace('\\', Storage.SeparatorChar); 105 | fullName = Storage.SeparatorChar + fullName.TrimStart(Storage.SeparatorChar); 106 | fixFullName = fullName; 107 | 108 | // find folder 109 | var dirName = Path.GetDirectoryName(fullName).Replace('\\', Storage.SeparatorChar); //change backslash to slash 110 | return PathUtil.AddLastSeparator(dirName); 111 | } 112 | 113 | public StorageEntryBase[] GetEntries(string searchPattern) 114 | { 115 | lock (_zipArchive) 116 | { 117 | 118 | var ret = new List<StorageEntryBase>(); 119 | var folders = new Dictionary<string, ZipArchiveEntry>(); 120 | foreach (var entry in _zipArchive.Entries) 121 | { 122 | var entryFolder = GetEntryFolderName(entry.FullName, out string fullName); 123 | if (entryFolder.IndexOf(_path, StringComparison.OrdinalIgnoreCase) != 0) 124 | continue; //not exists in current folder 125 | 126 | // find item part 127 | // if current folder is "/folder1/sub1/aa.txt" then itemPart is "sub1/aa.txt" 128 | var itemPart = fullName[_path.Length..].Replace('\\', Storage.SeparatorChar); 129 | if (string.IsNullOrEmpty(itemPart)) 130 | continue; //no item part means it posint to current storage 131 | 132 | // add file in current path 133 | if (entryFolder == _path && !string.IsNullOrEmpty(entry.Name)) // if entry.Name is empty it means it is empty folder not a file 134 | { 135 | ret.Add(StorageProviderEntryFromZipEntry(entry)); 136 | } 137 | // add folder in current path 138 | else 139 | { 140 | var nextSeparatorIndex = itemPart.IndexOf(Storage.SeparatorChar, StringComparison.OrdinalIgnoreCase); 141 | if (nextSeparatorIndex == -1) nextSeparatorIndex = itemPart.Length; 142 | var folderName = itemPart.Substring(0, nextSeparatorIndex); 143 | if (!folders.TryGetValue(folderName, out ZipArchiveEntry lastEntry) || lastEntry.LastWriteTime < entry.LastWriteTime) 144 | folders[folderName] = entry; 145 | } 146 | } 147 | 148 | // add folders 149 | foreach (var folder in folders) 150 | { 151 | ret.Add(new StorageEntryBase() 152 | { 153 | Attributes = 0, 154 | IsStorage = true, 155 | LastWriteTime = folder.Value.LastWriteTime.DateTime, 156 | Size = 0, 157 | Name = folder.Key, 158 | Uri = PathToUri(PathUtil.AddLastSeparator(Path.Combine(_path, folder.Key))) 159 | }); 160 | } 161 | 162 | return ret.ToArray(); 163 | } 164 | } 165 | 166 | public Stream OpenStream(Uri uri, StreamMode mode, StreamAccess access, StreamShare share, int bufferSize) 167 | { 168 | if (uri is null) throw new ArgumentNullException(nameof(uri)); 169 | 170 | if (mode != StreamMode.Open) 171 | throw new NotSupportedException($"ZipStorgeProvider does not support mode: {mode}"); 172 | 173 | lock (_zipArchive) 174 | { 175 | var path = PathFromUri(uri); 176 | var stream = _zipArchive.GetEntry(path).Open(); 177 | return stream; 178 | } 179 | } 180 | 181 | 182 | private string PathFromUri(Uri uri) 183 | { 184 | var query = HttpUtility.ParseQueryString(uri.Query); 185 | return query["fullname"]; 186 | } 187 | 188 | private Uri PathToUri(string path) 189 | { 190 | var uri = new UriBuilder(_streamUri?.ToString() ?? $"zip://zipstorgeprovider") 191 | { 192 | Query = $"fullname={path}" // we can't use uri path because some zip save "/" as back slash 193 | }; 194 | return uri.Uri; 195 | } 196 | 197 | private StorageEntryBase StorageProviderEntryFromZipEntry(ZipArchiveEntry entry) 198 | { 199 | StreamAttributes attr = 0; 200 | var ret = new StorageEntryBase() 201 | { 202 | Uri = PathToUri(entry.FullName), 203 | Attributes = attr, 204 | IsStorage = false, 205 | Name = entry.Name, 206 | LastWriteTime = entry.LastWriteTime.DateTime, 207 | Size = entry.Length 208 | }; 209 | 210 | return ret; 211 | } 212 | 213 | private bool _disposedValue = false; // To detect redundant calls 214 | protected virtual void Dispose(bool disposing) 215 | { 216 | if (_disposedValue) 217 | return; 218 | 219 | if (disposing) 220 | { 221 | // dispose managed state (managed objects). 222 | _zipArchive.Dispose(); 223 | } 224 | 225 | // free unmanaged resources (unmanaged objects) and override a finalizer below. 226 | // set large fields to null. 227 | 228 | _disposedValue = true; 229 | } 230 | 231 | public void Dispose() 232 | { 233 | Dispose(true); 234 | } 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /PortableStorage.Test/StorageTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using PortableStorage.Exceptions; 3 | using PortableStorage.Providers; 4 | using System; 5 | using System.IO; 6 | using System.Threading; 7 | 8 | namespace PortableStorage.Test 9 | { 10 | [TestClass] 11 | public class StorageTest 12 | { 13 | private static string TempPath => Path.Combine(Path.GetTempPath(), "_test_portablestroage"); 14 | 15 | private StorageRoot GetTempStorage(StorageOptions options = null) 16 | { 17 | var tempPath = Path.Combine(TempPath, Guid.NewGuid().ToString()); 18 | var storage = FileStorgeProvider.CreateStorage(tempPath, true, options); 19 | return storage; 20 | } 21 | 22 | [TestMethod] 23 | public void OpenStreamWrite_ByPath() 24 | { 25 | using var storage = GetTempStorage(); 26 | 27 | //check without path 28 | using (var ret = storage.OpenStreamWrite("filename.txt")) 29 | Assert.IsTrue(storage.EntryExists("filename.txt")); 30 | 31 | //check with path 32 | using (var ret = storage.OpenStreamWrite("foo1/foo2/foo3/filename2.txt")) 33 | { 34 | var storage2 = storage.OpenStorage("foo1/foo2/foo3"); 35 | Assert.IsFalse(storage.EntryExists("filename2.txt"), "file should not exist in root"); 36 | Assert.IsTrue(storage2.EntryExists("filename2.txt"), "file should exist in path"); 37 | } 38 | } 39 | 40 | [TestMethod] 41 | public void Rename_ByPath() 42 | { 43 | using var rootStorage = GetTempStorage(); 44 | var storage = rootStorage.CreateStorage("foo2"); 45 | 46 | storage.WriteAllText("foo3/foo4/filename1.txt", "123"); 47 | rootStorage.Rename("foo2/foo3/foo4/filename1.txt", "filename3.txt"); 48 | Assert.IsTrue(rootStorage.EntryExists("foo2/foo3/foo4/filename3.txt")); 49 | Assert.IsTrue(storage.EntryExists("foo3/foo4/filename3.txt")); 50 | 51 | rootStorage.Rename("foo2/foo3/foo4", "foo4-rename"); 52 | Assert.IsTrue(rootStorage.EntryExists("foo2/foo3/foo4-rename")); 53 | Assert.IsTrue(rootStorage.EntryExists("foo2/foo3/foo4-rename/filename3.txt")); 54 | } 55 | 56 | [TestMethod] 57 | public void Remove_ByPath() 58 | { 59 | using var rootStorage = GetTempStorage(); 60 | var storage = rootStorage.CreateStorage("foo3"); 61 | 62 | rootStorage.WriteAllText("foo3/foo4/filename1.txt", "123"); 63 | Assert.IsTrue(rootStorage.EntryExists("foo3/foo4/filename1.txt")); 64 | Assert.IsTrue(storage.EntryExists("foo4/filename1.txt")); 65 | 66 | storage.DeleteStream("foo4/filename1.txt"); 67 | Assert.IsFalse(rootStorage.EntryExists("foo3/foo4/filename1.txt")); 68 | Assert.IsFalse(storage.EntryExists("foo4/filename1.txt")); 69 | } 70 | 71 | [TestMethod] 72 | public void RootStorage_Path() 73 | { 74 | using var rootStorage = GetTempStorage(); 75 | var storage = rootStorage.CreateStorage("foo2"); 76 | storage.WriteAllText("foo3/foo4/filename1.txt", "123"); 77 | rootStorage.Rename("foo2/foo3/foo4/filename1.txt", "filename3.txt"); 78 | 79 | Assert.IsFalse(storage.EntryExists("foo2/foo3")); 80 | Assert.IsTrue(storage.EntryExists("/foo2/foo3")); 81 | Assert.AreEqual(storage.RootStorage.Path, Storage.SeparatorChar.ToString()); 82 | } 83 | 84 | [TestMethod] 85 | public void Create_stream_overwrite_existing() 86 | { 87 | using var rootStorage = GetTempStorage(); 88 | using (var stream = rootStorage.CreateStream("Test/foo1.txt", true)) 89 | stream.WriteByte(10); 90 | 91 | using var stream2 = rootStorage.OpenStreamRead("Test/foo1.txt"); 92 | Assert.AreEqual(stream2.ReadByte(), 10); 93 | } 94 | 95 | [TestMethod] 96 | public void Entry_of_root() 97 | { 98 | using var rootStorage = GetTempStorage(new StorageOptions() { IgnoreCase = false }); 99 | Assert.AreEqual(Storage.SeparatorChar.ToString(), rootStorage.Path); 100 | Assert.AreEqual(rootStorage, rootStorage.Entry.OpenStorage()); 101 | Assert.AreEqual(rootStorage.Path, rootStorage.Entry.Path); 102 | Assert.AreEqual(rootStorage.Uri, rootStorage.Entry.Uri); 103 | Assert.AreEqual(true, rootStorage.Entry.IsStorage); 104 | } 105 | 106 | [TestMethod] 107 | public void Case_sensitive() 108 | { 109 | using (var rootStorage = GetTempStorage(new StorageOptions() { IgnoreCase = false })) 110 | { 111 | rootStorage.WriteAllText("foo1/filename1.txt", "123"); 112 | Assert.IsTrue(rootStorage.EntryExists("foo1/filename1.txt")); 113 | Assert.IsFalse(rootStorage.EntryExists("foo1/Filename1.txt")); 114 | Assert.IsFalse(rootStorage.EntryExists("Foo1/filename1.txt")); 115 | } 116 | 117 | using (var rootStorage = GetTempStorage(new StorageOptions() { IgnoreCase = true })) 118 | { 119 | rootStorage.WriteAllText("foo1/filename1.txt", "123"); 120 | Assert.IsTrue(rootStorage.EntryExists("foo1/filename1.txt")); 121 | Assert.IsTrue(rootStorage.EntryExists("foo1/Filename1.txt")); 122 | Assert.IsTrue(rootStorage.EntryExists("Foo1/filename1.txt")); 123 | } 124 | } 125 | 126 | [TestMethod] 127 | public void WriteAllText_overwrite_oldfile() 128 | { 129 | using var rootStorage = GetTempStorage(); 130 | var path = "foo1/filename1.txt"; 131 | rootStorage.WriteAllText(path, "123456789"); 132 | rootStorage.WriteAllText(path, "123"); 133 | Assert.AreEqual(rootStorage.ReadAllText(path), "123"); 134 | } 135 | 136 | [TestMethod] 137 | public void Open_empty_path_should_throw_invalidpath() 138 | { 139 | using var rootStorage = GetTempStorage(); 140 | try 141 | { 142 | rootStorage.OpenStorage("", true); 143 | Assert.Fail("StorageNotExistsException expected"); 144 | } 145 | catch (ArgumentException ex) 146 | { 147 | Assert.AreEqual(ex.ParamName, "path"); 148 | } 149 | 150 | } 151 | 152 | [TestMethod] 153 | public void Storage_ClearCashe() 154 | { 155 | using var rootStorage = GetTempStorage(); 156 | rootStorage.CreateStorage("a/b/c"); 157 | Assert.IsTrue(rootStorage.EntryExists("a/b/c")); 158 | 159 | // perform unmanage delete 160 | Directory.Delete(rootStorage.Uri.LocalPath + @"a\b\c"); 161 | 162 | rootStorage.ClearCache(); 163 | 164 | // check StorageNotFoundException for OpenStorage 165 | try 166 | { 167 | var storage = rootStorage.OpenStorage("a/b/c", false); 168 | Assert.Fail("StorageNotFoundException exception expected!"); 169 | } 170 | catch(StorageNotFoundException) 171 | { 172 | } 173 | 174 | Assert.IsFalse(rootStorage.EntryExists("a/b/c"), "entry should not exists"); 175 | } 176 | 177 | [TestMethod] 178 | public void EntrySizeAndWriteTime_Updates_ByStream() 179 | { 180 | using var rootStorage = GetTempStorage(); 181 | var stream = rootStorage.CreateStream("aaa"); 182 | var entry = rootStorage.GetStreamEntry("aaa"); 183 | var lastWriteTime = entry.LastWriteTime; 184 | Assert.AreEqual(0, entry.Size); 185 | 186 | var buf = new byte[] { 1, 2, 3, 4, 5 }; 187 | Thread.Sleep(1000); 188 | stream.Write(buf, 0, buf.Length); 189 | Assert.AreEqual(5, entry.Size); 190 | Assert.AreNotEqual(lastWriteTime, entry.LastWriteTime); 191 | } 192 | 193 | [TestMethod] 194 | public void Storage_GetEntry_must_have_entry_name () 195 | { 196 | using var storage = GetTempStorage(); 197 | storage.WriteAllText("/aa/b1.txt", "123"); 198 | storage.WriteAllText("/aa/b2.txt", "123"); 199 | storage.WriteAllText("/aa/b3.txt", "123"); 200 | 201 | try 202 | { 203 | var entry = storage.GetEntry("/aa/"); 204 | Assert.Fail("ArgumentException exception was expected!"); 205 | } 206 | catch (ArgumentException){} 207 | } 208 | 209 | [TestMethod] 210 | public void CopyTo() 211 | { 212 | using var destStorage = GetTempStorage(); 213 | using var rootStorage = GetTempStorage(); 214 | var text = "123456789"; 215 | var path = "foo1/filename1.txt"; 216 | rootStorage.WriteAllText(path, text); 217 | rootStorage.WriteAllText("foo1/foo11/filename111.txt", text); 218 | rootStorage.Copy(path, "foo1/filename2.txt"); 219 | Assert.AreEqual(rootStorage.ReadAllText("foo1/filename2.txt"), text); 220 | 221 | rootStorage.Copy(path, "foo2/"); 222 | Assert.AreEqual(rootStorage.ReadAllText("foo2/filename1.txt"), text); 223 | 224 | rootStorage.Copy(path, "foo3/foo4/zz.txt"); 225 | Assert.AreEqual(rootStorage.ReadAllText("foo3/foo4/zz.txt"), text); 226 | 227 | rootStorage.Copy(path, destStorage, "foo1/"); 228 | Assert.AreEqual(destStorage.ReadAllText("foo1/filename1.txt"), text); 229 | 230 | rootStorage.Copy(path, destStorage, "foo1/zz.txt"); 231 | Assert.AreEqual(destStorage.ReadAllText("foo1/zz.txt"), text); 232 | 233 | rootStorage.Copy("foo1", "zfoo1/"); 234 | Assert.AreEqual(rootStorage.ReadAllText("zfoo1/foo1/foo11/filename111.txt"), text); 235 | 236 | rootStorage.Copy("foo1", "zfoo2"); 237 | Assert.AreEqual(rootStorage.ReadAllText("zfoo2/foo11/filename111.txt"), text); 238 | 239 | rootStorage.CopyTo(destStorage, "foo1_copy"); 240 | Assert.AreEqual(destStorage.ReadAllText("foo1_copy/foo1/foo11/filename111.txt"), text); 241 | 242 | rootStorage.CopyTo(destStorage.CreateStorage("total")); 243 | Assert.AreEqual(destStorage.ReadAllText("total/foo1/foo11/filename111.txt"), text); 244 | } 245 | 246 | [ClassCleanup] 247 | public static void ClassCleanup() 248 | { 249 | if (Directory.Exists(TempPath)) 250 | Directory.Delete(TempPath, true); 251 | } 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /PortableStorage.Android/SAFStorgeProvider.cs: -------------------------------------------------------------------------------- 1 | using Android.Content; 2 | using Android.Provider; 3 | using PortableStorage.Exceptions; 4 | using PortableStorage.Providers; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Text.RegularExpressions; 10 | 11 | namespace PortableStorage.Droid 12 | { 13 | public class SafStorgeProvider : IStorageProvider 14 | { 15 | public Context Context { get; } 16 | public Android.Net.Uri AndroidUri { get; } 17 | public Uri Uri => AndroidUriToUri(AndroidUri); 18 | public bool IsGetEntriesBySearchPatternFast => false; 19 | public bool IsGetEntryUriByNameFast => false; 20 | private string _name; 21 | 22 | public static StorageRoot CreateStorage(Context context, Uri uri, StorageOptions storageOptions = null) 23 | { 24 | return CreateStorage(context, AndroidUriFromUri(uri), storageOptions); 25 | } 26 | 27 | public static StorageRoot CreateStorage(Context context, Android.Net.Uri androidUri, StorageOptions storageOptions = null) 28 | { 29 | var provider = new SafStorgeProvider(context, androidUri); 30 | return new StorageRoot(provider, storageOptions); 31 | } 32 | 33 | public SafStorgeProvider(Context context, Uri uri) 34 | : this(context, AndroidUriFromUri(uri)) 35 | { 36 | } 37 | 38 | public SafStorgeProvider(Context context, Android.Net.Uri androidUri) 39 | : this(context, androidUri, null) 40 | { 41 | } 42 | 43 | private SafStorgeProvider(Context context, Android.Net.Uri androidUri, string name) 44 | { 45 | AndroidUri = androidUri; 46 | Context = context; 47 | _name = name; 48 | } 49 | 50 | public CreateStorageResult CreateStorage(string name) 51 | { 52 | var docUri = DocumentsContract.CreateDocument(Context.ContentResolver, AndroidUri, DocumentsContract.Document.MimeTypeDir, name); 53 | if (docUri == null) 54 | throw new Exception($"Could not create storage. Uri: {Uri}, name: {name}"); 55 | 56 | var ret = new CreateStorageResult() 57 | { 58 | Storage = new SafStorgeProvider(Context, docUri, name), 59 | Entry = new StorageEntryBase 60 | { 61 | Name = name, 62 | LastWriteTime = DateTime.Now, 63 | Size = 0, 64 | IsStorage = true, 65 | Uri = AndroidUriToUri(docUri), 66 | } 67 | }; 68 | 69 | return ret; 70 | } 71 | 72 | public CreateStreamResult CreateStream(string name, StreamAccess access, StreamShare share, int bufferSize = 0) 73 | { 74 | var resolver = Context.ContentResolver; 75 | var androidUri = DocumentsContract.CreateDocument(resolver, AndroidUri, "application/octet-stream", name); 76 | var fs = OpenStream(androidUri, StreamMode.Open, access, share, bufferSize); 77 | var ret = new CreateStreamResult() 78 | { 79 | Stream = fs, 80 | EntryBase = new StorageEntryBase 81 | { 82 | Name = name, 83 | LastWriteTime = DateTime.Now, 84 | Size = 0, 85 | IsStorage = false, 86 | Uri = AndroidUriToUri(androidUri) 87 | } 88 | }; 89 | return ret; 90 | } 91 | 92 | public IStorageProvider OpenStorage(Uri uri) 93 | { 94 | return OpenStorage(AndroidUriFromUri(uri)); 95 | } 96 | 97 | private IStorageProvider OpenStorage(Android.Net.Uri docUri) 98 | { 99 | return new SafStorgeProvider(Context, docUri); 100 | } 101 | 102 | public string Name 103 | { 104 | get 105 | { 106 | if (_name != null) 107 | return _name; 108 | 109 | var docUri = AndroidUri; 110 | var projection = new string[] { DocumentsContract.Document.ColumnDisplayName }; 111 | using (var cursor = Context.ContentResolver.Query(docUri, projection, null, null, null)) 112 | { 113 | while (cursor.MoveToNext()) 114 | { 115 | _name = cursor.GetString(0); 116 | cursor.Close(); 117 | return _name; 118 | } 119 | cursor.Close(); 120 | } 121 | throw new IOException($"Could not read name. URI: {docUri}"); 122 | } 123 | } 124 | 125 | public long GetFreeSpace() 126 | { 127 | using var fd = Context.ContentResolver.OpenFileDescriptor(AndroidUri, "r"); 128 | var stat = Android.Systems.Os.Fstatvfs(fd.FileDescriptor); 129 | return stat.FBavail * stat.FBsize; 130 | } 131 | 132 | public Uri Rename(Uri uri, string desName) 133 | { 134 | return AndroidUriToUri(Rename(AndroidUriFromChildNetUri(uri), desName)); 135 | } 136 | 137 | public Android.Net.Uri Rename(Android.Net.Uri docUri, string desName) 138 | { 139 | var ret = DocumentsContract.RenameDocument(Context.ContentResolver, docUri, desName); 140 | if (ret == null) 141 | throw new Exception($"Could not rename storage or stream. Uri: {Uri}"); 142 | _name = desName; 143 | return ret; 144 | } 145 | 146 | public void RemoveStream(Uri uri) 147 | { 148 | RemoveStream(AndroidUriFromChildNetUri(uri)); 149 | } 150 | 151 | private void RemoveStream(Android.Net.Uri docUri) 152 | { 153 | //some storage (maybe older android) does not free space till truncate the file. it is a temporaray solution 154 | //var stream = OpenStream(docUri, StreamMode.Truncate, StreamAccess.Write, StreamShare.None); 155 | //stream.Dispose(); 156 | 157 | if (!DocumentsContract.DeleteDocument(Context.ContentResolver, docUri)) 158 | throw new Exception($"Could not delete stream. Uri: {docUri}"); 159 | } 160 | 161 | 162 | public void RemoveStorage(Uri uri) 163 | { 164 | RemoveStorage(AndroidUriFromChildNetUri(uri)); 165 | } 166 | 167 | public void RemoveStorage(Android.Net.Uri docUri) 168 | { 169 | //some OTG flags does not release cause lost directory so remove directory recursively 170 | var subStorages = GetEntries().Where(x => x.IsStorage).Select(x => (SafStorgeProvider)OpenStorage(docUri)); 171 | foreach (var subStorage in subStorages) 172 | { 173 | subStorage.EraseStorage(); 174 | subStorage.Dispose(); 175 | } 176 | 177 | if (!DocumentsContract.DeleteDocument(Context.ContentResolver, docUri)) 178 | throw new Exception($"Could not delete storage. Uri: {docUri}"); 179 | } 180 | 181 | private void EraseStorage() 182 | { 183 | //erase substorage 184 | foreach (var entry in GetEntries()) 185 | { 186 | if (entry.IsStorage) 187 | RemoveStorage(entry.Uri); 188 | //else 189 | // RemoveStream(item.Uri); 190 | } 191 | } 192 | 193 | public Uri GetEntryUriByName(string name) 194 | { 195 | var entries = GetEntries(); 196 | var entry = entries.Where(x => x.Name == name).FirstOrDefault(); 197 | if (entry != null) 198 | return entry.Uri; 199 | 200 | throw new StorageNotFoundException(Uri, name); 201 | } 202 | 203 | public Stream OpenStream(Uri uri, StreamMode mode, StreamAccess access, StreamShare share, int bufferSize = 0) 204 | { 205 | return OpenStream(AndroidUriFromChildNetUri(uri), mode, access, share, bufferSize); 206 | } 207 | 208 | private Stream OpenStream(Android.Net.Uri androidUri, StreamMode mode, StreamAccess access, StreamShare _, int bufferSize = 0) 209 | { 210 | if (access == StreamAccess.ReadWrite) 211 | throw new ArgumentException("StreamMode.ReadWrite does not support!"); 212 | 213 | 214 | var resolver = Context.ContentResolver; 215 | string streamMode = ""; 216 | if (bufferSize == 0) bufferSize = 4096; 217 | 218 | switch (mode) 219 | { 220 | case StreamMode.Append: 221 | if (access == StreamAccess.Read) throw new ArgumentException("StreamMode.Append only support StreamAccess.write access."); 222 | if (access == StreamAccess.Write) streamMode = "wa"; 223 | if (access == StreamAccess.ReadWrite) throw new ArgumentException("StreamMode.Append only support StreamAccess.write access."); 224 | break; 225 | 226 | case StreamMode.Open: 227 | if (access == StreamAccess.Read) streamMode = "r"; 228 | if (access == StreamAccess.Write) streamMode = "rw"; //rw instead w; because w does not support seek 229 | break; 230 | 231 | case StreamMode.Truncate: 232 | if (access == StreamAccess.Read) throw new ArgumentException("StreamMode.Truncate does not support StreamAccess.read access."); 233 | if (access == StreamAccess.ReadWrite) throw new ArgumentException("StreamMode.Truncate does not support StreamAccess.readWrite access."); 234 | if (access == StreamAccess.Write) streamMode = "rwt"; 235 | break; 236 | } 237 | 238 | 239 | var parcelFD = resolver.OpenFileDescriptor(androidUri, streamMode); 240 | var stream = new ChannelStream(parcelFD, streamMode); 241 | var ret = (Stream)new BufferedStream(stream, bufferSize); 242 | return ret; 243 | } 244 | 245 | public void SetAttributes(Uri uri, StreamAttributes attributes) 246 | { 247 | throw new NotSupportedException(); 248 | } 249 | 250 | public StorageEntryBase[] GetEntries(string searchPattern = null) 251 | { 252 | return GetEntriesImpl(searchPattern); 253 | } 254 | 255 | private StorageEntryBase[] GetEntriesImpl(string searchPattern = null) 256 | { 257 | 258 | var itemProperties = new List<StorageEntryBase>(); 259 | 260 | var childrenUri = DocumentsContract.BuildChildDocumentsUriUsingTree(AndroidUri, DocumentsContract.GetDocumentId(AndroidUri)); 261 | var projection = new string[] { 262 | DocumentsContract.Document.ColumnDocumentId, 263 | DocumentsContract.Document.ColumnDisplayName, 264 | DocumentsContract.Document.ColumnMimeType, 265 | DocumentsContract.Document.ColumnSize, 266 | DocumentsContract.Document.ColumnLastModified }; 267 | 268 | //build searchPattern 269 | string selection = null; 270 | string[] selectionArgs = null; 271 | var regXpattern = searchPattern != null ? Storage.WildcardToRegex(searchPattern) : null; 272 | //it looks doesn't supported for storage 273 | //if (!string.IsNullOrEmpty(searchPattern)) 274 | //{ 275 | // selection = $"{DocumentsContract.Document.ColumnDocumentId}=?"; 276 | // selectionArgs = new string[] { searchPattern.Replace("*", "%") }; 277 | //} 278 | 279 | 280 | //run the query 281 | using (var cursor = Context.ContentResolver.Query(childrenUri, projection, selection, selectionArgs, null)) 282 | { 283 | while (cursor.MoveToNext()) 284 | { 285 | var documentId = cursor.GetString(0); 286 | var name = cursor.GetString(1); 287 | 288 | if (regXpattern != null && !Regex.IsMatch(name, regXpattern)) 289 | continue; 290 | 291 | StreamAttributes attribute = 0; 292 | if (!string.IsNullOrEmpty(name) && name[0] == '.') attribute |= StreamAttributes.Hidden; 293 | 294 | itemProperties.Add( 295 | new StorageEntryBase() 296 | { 297 | Name = name, 298 | Uri = AndroidUriToUri(DocumentsContract.BuildDocumentUriUsingTree(AndroidUri, documentId)), 299 | IsStorage = cursor.GetString(2) == DocumentsContract.Document.MimeTypeDir, 300 | Size = long.Parse(cursor.GetString(3)), 301 | LastWriteTime = cursor.GetString(4) != null ? JavaTimeStampToDateTime(double.Parse(cursor.GetString(4))) : DateTime.Now, 302 | Attributes = attribute, 303 | }); 304 | 305 | if (!string.IsNullOrEmpty(searchPattern)) 306 | break; 307 | } 308 | cursor.Close(); 309 | } 310 | 311 | return itemProperties.ToArray(); 312 | } 313 | 314 | private Android.Net.Uri AndroidUriFromChildNetUri(Uri uri) 315 | { 316 | return AndroidUriFromUri(uri); 317 | } 318 | 319 | private static DateTime JavaTimeStampToDateTime(double javaTimeStamp) 320 | { 321 | // Java timestamp is millisecods past epoch 322 | DateTime dtDateTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); 323 | dtDateTime = dtDateTime.AddSeconds(Math.Round(javaTimeStamp / 1000)).ToLocalTime(); 324 | return dtDateTime; 325 | } 326 | 327 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Code Quality", "IDE0051:Remove unused private members", Justification = "<Pending>")] 328 | private bool IsStorageUri(Android.Net.Uri docUri) 329 | { 330 | var res = Context.ContentResolver.GetType(docUri); 331 | return res != null && Context.ContentResolver.GetType(docUri) == DocumentsContract.Document.MimeTypeDir; 332 | } 333 | 334 | private static Android.Net.Uri AndroidUriFromUri(Uri uri) 335 | { 336 | //todo: check parent id 337 | return Android.Net.Uri.Parse(uri.ToString()); 338 | } 339 | 340 | private static Uri AndroidUriToUri(Android.Net.Uri androidUri) 341 | { 342 | return new Uri(androidUri.ToString()); 343 | } 344 | 345 | public void Dispose() 346 | { 347 | } 348 | } 349 | } 350 | -------------------------------------------------------------------------------- /PortableStorage/Storage.cs: -------------------------------------------------------------------------------- 1 | using PortableStorage.Exceptions; 2 | using System; 3 | using System.Collections.Concurrent; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Text.RegularExpressions; 9 | using PortableStorage.Providers; 10 | using System.Collections.ObjectModel; 11 | using System.Globalization; 12 | 13 | namespace PortableStorage 14 | { 15 | public class Storage 16 | { 17 | public static readonly char SeparatorChar = '/'; 18 | public int CacheTimeout => RootStorage._cacheTimeoutFiled; 19 | public Storage Parent { get; } 20 | 21 | public bool IgnoreCase => RootStorage._ignoreCase; 22 | public Type ProviderType => _provider.GetType(); 23 | 24 | private readonly bool _ignoreCase; 25 | private readonly IStorageProvider _provider; 26 | private readonly bool _leaveProviderOpen; 27 | private readonly int _cacheTimeoutFiled; 28 | private DateTime _lastCacheTime = DateTime.MinValue; 29 | private readonly ConcurrentDictionary<string, Storage> _storageCache = new ConcurrentDictionary<string, Storage>(); 30 | private readonly ConcurrentDictionary<string, StorageEntry> _entryCache = new ConcurrentDictionary<string, StorageEntry>(); 31 | private readonly object _lockObject = new object(); 32 | private string _name; 33 | private readonly bool _isVirtual; 34 | public bool IsVirtual => Parent != null && (Parent.IsVirtual || _isVirtual); 35 | 36 | public Storage(IStorageProvider provider, StorageOptions options) 37 | { 38 | options ??= new StorageOptions(); 39 | _provider = provider ?? throw new ArgumentNullException(nameof(provider)); 40 | _cacheTimeoutFiled = options.CacheTimeout == -1 ? 1000 : options.CacheTimeout; 41 | _virtualStorageProviders = new ReadOnlyDictionary<string, IVirtualStorageProvider>( options.VirtualStorageProviders ); 42 | _ignoreCase = options.IgnoreCase; 43 | _leaveProviderOpen = options.LeaveProviderOpen; 44 | } 45 | 46 | private Storage(IStorageProvider provider, Storage parent, bool leaveProviderOpen, bool isVirtual) 47 | { 48 | _provider = provider ?? throw new ArgumentNullException(nameof(provider)); 49 | Parent = parent ?? throw new ArgumentNullException(nameof(parent)); 50 | _leaveProviderOpen = leaveProviderOpen; 51 | _isVirtual = isVirtual; 52 | } 53 | 54 | private IReadOnlyDictionary<string, IVirtualStorageProvider> _virtualStorageProviders; 55 | public IReadOnlyDictionary<string, IVirtualStorageProvider> VirtualStorageProviders 56 | { 57 | get => RootStorage._virtualStorageProviders; 58 | set 59 | { 60 | RootStorage._virtualStorageProviders = value; 61 | ClearCache(); 62 | } 63 | } 64 | 65 | public string Path => (Parent == null) ? SeparatorChar.ToString(CultureInfo.CurrentCulture) : PathCombine(Parent.Path, Name); 66 | public bool IsRoot => Parent == null; 67 | public StorageRoot RootStorage => Parent?.RootStorage ?? (StorageRoot)this; 68 | 69 | public string Name 70 | { 71 | get 72 | { 73 | lock (_lockObject) 74 | { 75 | if (_name == null) 76 | _name = _provider.Name; // cache the name, it might be so slow 77 | return _name; 78 | } 79 | } 80 | } 81 | 82 | public Uri Uri => _provider.Uri; 83 | 84 | private bool IsCacheAvailable 85 | { 86 | get 87 | { 88 | lock (_lockObject) 89 | return CacheTimeout != 0 && _lastCacheTime.AddSeconds(CacheTimeout) > DateTime.Now; 90 | } 91 | } 92 | 93 | private Storage GetStorageForPath(string path, out string name, bool createIfNotExists = false) 94 | { 95 | // fix backslash 96 | path = path.Replace('\\', SeparatorChar); 97 | 98 | // manage path from root 99 | if (path.Length > 0 && path[0] == SeparatorChar) 100 | return RootStorage.GetStorageForPath(path.Remove(0, 1), out name, createIfNotExists); // back to root 101 | 102 | var parentPath = System.IO.Path.GetDirectoryName(path); 103 | name = System.IO.Path.GetFileName(path); 104 | if (!string.IsNullOrEmpty(parentPath)) 105 | return createIfNotExists ? CreateStorage(parentPath) : OpenStorage(parentPath); 106 | 107 | return null; 108 | } 109 | 110 | public Stream OpenStream(string path, StreamMode mode, StreamAccess access, StreamShare share, int bufferSize = 0) 111 | { 112 | // manage path 113 | var storage = GetStorageForPath(path, out string name); 114 | if (storage != null) 115 | return storage.OpenStream(name, mode, access, share, bufferSize); 116 | 117 | //check mode 118 | if (mode == StreamMode.Append || mode == StreamMode.Truncate) 119 | if (access != StreamAccess.Write && access != StreamAccess.ReadWrite) 120 | throw new ArgumentException($"{mode} needs StreamAccess.write access."); 121 | 122 | var entry = GetStreamEntry(name); 123 | try 124 | { 125 | var result = _provider.OpenStream(entry.Uri, mode, access, share, bufferSize); 126 | var ret = new StreamController(result, entry); 127 | return ret; 128 | } 129 | catch (StorageNotFoundException) 130 | { 131 | ClearCache(false); 132 | throw; 133 | } 134 | } 135 | 136 | public long GetFreeSpace() 137 | { 138 | return _provider.GetFreeSpace(); 139 | } 140 | 141 | /// <param name="searchPattern">can include path and wildcard. eg: /folder/file.*</param> 142 | public StorageEntry[] GetStorageEntries(string searchPattern = null) 143 | { 144 | return GetEntries(searchPattern).Where(x => x.IsStorage).ToArray(); 145 | } 146 | 147 | /// <param name="searchPattern">can include path and wildcard. eg: /folder/file.*</param> 148 | public StorageEntry[] GetStreamEntries(string searchPattern = null) 149 | { 150 | return GetEntries(searchPattern).Where(x => !x.IsStorage).ToArray(); 151 | } 152 | 153 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1819:Properties should not return arrays", Justification = "<Pending>")] 154 | public StorageEntry[] Entries => GetEntries(null); 155 | 156 | 157 | /// <param name="searchPattern">can include path and wildcard. eg: /folder/file.*</param> 158 | public StorageEntry[] GetEntries(string searchPattern) 159 | { 160 | // manage path 161 | if (!string.IsNullOrEmpty(searchPattern)) 162 | { 163 | var storage = GetStorageForPath(searchPattern, out string newSearchPattern); 164 | if (storage != null) 165 | return storage.GetEntries(newSearchPattern); 166 | searchPattern = newSearchPattern; 167 | } 168 | 169 | //check is cache available 170 | lock (_lockObject) 171 | { 172 | if (IsCacheAvailable) 173 | { 174 | if (!string.IsNullOrEmpty(searchPattern)) 175 | { 176 | var regexOptions = IgnoreCase ? RegexOptions.IgnoreCase : RegexOptions.None; 177 | var regXpattern = WildcardToRegex(searchPattern); 178 | return _entryCache.Where(x => Regex.IsMatch(x.Key, regXpattern, regexOptions)).Select(x => x.Value).ToArray(); 179 | } 180 | return _entryCache.Select(x => x.Value).ToArray(); 181 | } 182 | } 183 | 184 | //use provider 185 | var pattern = _provider.IsGetEntriesBySearchPatternFast ? searchPattern : null; 186 | pattern = null; 187 | var providerEntires = _provider.GetEntries(pattern); 188 | var entries = StorageEntryFromStorageEntryProvider(providerEntires); 189 | 190 | //update cache 191 | _entryCache.Clear(); 192 | foreach (var entry in entries) 193 | AddToCache(entry); 194 | 195 | //cache the result when all item is returned 196 | if (pattern == null) 197 | { 198 | _lastCacheTime = DateTime.Now; 199 | 200 | //apply searchPattern 201 | if (searchPattern != null) 202 | { 203 | var regexOptions = IgnoreCase ? RegexOptions.IgnoreCase : RegexOptions.None; 204 | var regXpattern = WildcardToRegex(searchPattern); 205 | entries = entries.Where(x => Regex.IsMatch(x.Name, regXpattern, regexOptions)).ToArray(); 206 | } 207 | } 208 | 209 | return entries; 210 | } 211 | 212 | /// <summary> 213 | /// return null for root storage 214 | /// </summary> 215 | public StorageEntry Entry => IsRoot ? RootEntry : Parent.GetEntry(Name); 216 | 217 | private StorageEntry RootEntry => new StorageEntry() 218 | { 219 | IsStorage = true, 220 | IsVirtualStorage = RootStorage.IsVirtual, 221 | IsStream = false, 222 | LastWriteTime = null, 223 | Uri = RootStorage.Uri, 224 | Name = "", 225 | Size = 0, 226 | Root = RootStorage, 227 | Parent = null, 228 | Path = RootStorage.Path 229 | }; 230 | 231 | public void ClearCache(bool recursive = true) 232 | { 233 | lock (_lockObject) 234 | { 235 | if (recursive) 236 | { 237 | foreach (var item in _storageCache) 238 | item.Value.ClearCache(recursive); 239 | } 240 | 241 | _storageCache.Clear(); 242 | _entryCache.Clear(); 243 | _lastCacheTime = DateTime.MinValue; 244 | } 245 | } 246 | 247 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "<Pending>")] 248 | public Storage OpenStorage(string path) 249 | { 250 | // manage path 251 | var storage = GetStorageForPath(path, out string name); 252 | if (storage != null) 253 | return storage.OpenStorage(name); 254 | 255 | //use storage cache 256 | var storageFromCache = StorageCache_Get(name); 257 | if (storageFromCache != null) 258 | return storageFromCache; 259 | 260 | // open storage and add it to cache 261 | lock (_lockObject) 262 | { 263 | var storageEntry = GetStorageEntry(name); 264 | var uri = storageEntry.Uri; 265 | try 266 | { 267 | Storage newStorage; 268 | if (storageEntry.IsVirtualStorage && 269 | VirtualStorageProviders.TryGetValue(System.IO.Path.GetExtension(name), out IVirtualStorageProvider virtualStorageProvider)) 270 | { 271 | var stream = OpenStreamRead(name); 272 | var storageProvider = virtualStorageProvider.CreateStorageProvider(stream, storageEntry.Uri, name); 273 | newStorage = new Storage(storageProvider, this, false, true); 274 | } 275 | else 276 | { 277 | var storageProvider = _provider.OpenStorage(uri); 278 | newStorage = new Storage(storageProvider, this, true, false); 279 | } 280 | 281 | StorageCache_Add(name, newStorage); 282 | return newStorage; 283 | } 284 | catch (StorageNotFoundException) 285 | { 286 | ClearCache(false); 287 | throw; 288 | } 289 | } 290 | } 291 | 292 | public Storage CreateStorage(string path, bool openIfAlreadyExists = true) 293 | { 294 | // manage path 295 | var storage = GetStorageForPath(path, out string name, true); 296 | if (storage != null) 297 | return storage.CreateStorage(name, openIfAlreadyExists); 298 | 299 | // validate name 300 | if (string.IsNullOrEmpty(path?.Trim())) 301 | throw new ArgumentException("invalid path name!", nameof(path)); 302 | 303 | // Check existance, some provider may duplicate the entry with same name 304 | if (EntryExists(name)) 305 | { 306 | if (openIfAlreadyExists) 307 | return OpenStorage(name); 308 | else 309 | throw new IOException("Entry already exists!"); 310 | } 311 | 312 | var result = _provider.CreateStorage(name); 313 | var entry = ProviderEntryToEntry(result.Entry); 314 | var newStorage = new Storage(result.Storage, this, true, false); 315 | 316 | StorageCache_Add(name, newStorage); 317 | AddToCache(entry); 318 | return newStorage; 319 | } 320 | 321 | private void StorageCache_Add(string name, Storage storage) 322 | { 323 | lock (_lockObject) 324 | { 325 | StorageCache_Remove(name); 326 | _storageCache.TryAdd(name, storage); 327 | } 328 | } 329 | 330 | private void StorageCache_Remove(string name) 331 | { 332 | if (_storageCache.TryRemove(name, out Storage storage)) 333 | storage.Close(); 334 | } 335 | 336 | private Storage StorageCache_Get(string name) 337 | { 338 | lock (_lockObject) 339 | { 340 | if (_storageCache.TryGetValue(name, out Storage storage)) 341 | { 342 | if (!storage._disposedValue) 343 | return storage; 344 | 345 | _storageCache.TryRemove(name, out _); 346 | } 347 | } 348 | return null; 349 | } 350 | 351 | private void AddToCache(StorageEntry entry) 352 | { 353 | _entryCache.TryAdd(entry.Name, entry); 354 | } 355 | 356 | 357 | public void DeleteStream(string path) 358 | { 359 | Delete(path, false); 360 | } 361 | 362 | public void DeleteStorage(string path) 363 | { 364 | Delete(path, true); 365 | } 366 | 367 | private void Delete(string path, bool isStorage) 368 | { 369 | // manage path 370 | var storage = GetStorageForPath(path, out string name); 371 | if (storage != null) 372 | { 373 | storage.Delete(name, isStorage); 374 | return; 375 | } 376 | 377 | // get the entry 378 | var entry = isStorage ? GetStorageEntry(name) : GetStreamEntry(name); 379 | 380 | //update cache 381 | StorageCache_Remove(name); 382 | _entryCache.TryRemove(name, out _); 383 | 384 | // remove physically after disposing objects 385 | if (isStorage) 386 | _provider.RemoveStorage(entry.Uri); 387 | else 388 | _provider.RemoveStream(entry.Uri); 389 | 390 | } 391 | 392 | public void Rename(string path, string desName) 393 | { 394 | // manage path 395 | var storage = GetStorageForPath(path, out string name); 396 | if (storage != null) 397 | { 398 | storage.Rename(name, desName); 399 | return; 400 | } 401 | 402 | // rename physically 403 | var entry = GetEntry(name); 404 | 405 | //As storage.uri will be change the descendant may change too which can not be cached 406 | StorageCache_Remove(name); 407 | 408 | // update the cache 409 | lock (_lockObject) 410 | { 411 | var isVirtualStorage = VirtualStorageProviders.TryGetValue(System.IO.Path.GetExtension(desName), out _) || IsVirtual; 412 | 413 | var newUri = _provider.Rename(entry.Uri, desName); 414 | if (_entryCache.TryRemove(name, out StorageEntry storageEntry)) 415 | { 416 | entry.Name = desName; 417 | entry.Uri = newUri; 418 | entry.Path = PathCombine(Path, desName); 419 | entry.IsStorage = entry.IsStorage || isVirtualStorage; 420 | entry.IsVirtualStorage = isVirtualStorage; 421 | AddToCache(entry); 422 | } 423 | } 424 | } 425 | 426 | public bool EntryExists(string path) 427 | { 428 | try 429 | { 430 | return GetEntry(path) != null; 431 | } 432 | catch (StorageNotFoundException) 433 | { 434 | return false; 435 | } 436 | } 437 | 438 | public bool StorageExists(string path) 439 | { 440 | try 441 | { 442 | return GetStorageEntry(path) != null; 443 | } 444 | catch (StorageNotFoundException) 445 | { 446 | return false; 447 | } 448 | } 449 | 450 | public bool StreamExists(string path) 451 | { 452 | try 453 | { 454 | return GetStreamEntry(path) != null; 455 | } 456 | catch (StorageNotFoundException) 457 | { 458 | return false; 459 | } 460 | } 461 | 462 | 463 | public StorageEntry GetStorageEntry(string path) => GetEntryHelper(path, true, false); 464 | public bool TryGetStorageEntry(string path, out StorageEntry storageEntry) => TryGetEntryHelper(path, true, false, out storageEntry); 465 | 466 | public StorageEntry GetStreamEntry(string path) => GetEntryHelper(path, false, true); 467 | public bool TryGetStreamEntry(string path, out StorageEntry storageEntry) => TryGetEntryHelper(path, false, true, out storageEntry); 468 | 469 | public StorageEntry GetEntry(string path) => GetEntryHelper(path, true, true); 470 | public bool TryGetEntry(string path, out StorageEntry storageEntry) => TryGetEntryHelper(path, true, true, out storageEntry); 471 | 472 | private bool TryGetEntryHelper(string path, bool includeStorage, bool includeStream, out StorageEntry storageEntry) 473 | { 474 | try 475 | { 476 | storageEntry = GetEntryHelper(path, includeStorage, includeStream); 477 | return true; 478 | } 479 | catch (StorageNotFoundException) 480 | { 481 | storageEntry = null; 482 | return false; 483 | } 484 | } 485 | 486 | private StorageEntry GetEntryHelper(string path, bool includeStorage, bool includeStream) 487 | { 488 | // manage path 489 | var storage = GetStorageForPath(path, out string name); 490 | if (storage != null) 491 | return storage.GetEntryHelper(name, includeStorage, includeStream); 492 | 493 | // throw error for empty path 494 | if (string.IsNullOrEmpty(path)) 495 | throw new ArgumentException("Could not find entry name in the path!", nameof(path)); 496 | 497 | // manage by name 498 | var entries = GetEntries(path); 499 | var item = entries.FirstOrDefault(); 500 | if (item != null && includeStorage && item.IsStorage) return item; 501 | if (item != null && includeStream && item.IsStream) return item; 502 | throw new StorageNotFoundException(Uri, path); 503 | } 504 | 505 | public void SetAttributes(string path, StreamAttributes attributes) 506 | { 507 | var entry = GetEntry(path); 508 | try 509 | { 510 | _provider.SetAttributes(entry.Uri, attributes); 511 | entry.Attributes = attributes; 512 | } 513 | catch (NotSupportedException) 514 | { 515 | } 516 | } 517 | 518 | public StreamAttributes GetAttributes(string path) 519 | { 520 | var entry = GetEntry(path); 521 | var ret = entry.Attributes; 522 | return ret; 523 | } 524 | 525 | public Stream OpenStreamRead(string path, int bufferSize = 0) 526 | { 527 | return OpenStream(path, StreamMode.Open, StreamAccess.Read, StreamShare.Read, bufferSize); 528 | } 529 | 530 | public Stream OpenStreamWrite(string path, bool truncateIfExists = false) 531 | { 532 | try 533 | { 534 | return OpenStream(path, truncateIfExists ? StreamMode.Truncate : StreamMode.Open, StreamAccess.Write, StreamShare.None); 535 | } 536 | catch (StorageNotFoundException) 537 | { 538 | return CreateStream(path, truncateIfExists); 539 | } 540 | } 541 | 542 | public Stream CreateStream(string name, bool overwriteExisting = false, int bufferSize = 0) 543 | { 544 | return CreateStream(name, StreamShare.None, overwriteExisting, bufferSize); 545 | } 546 | 547 | public Stream CreateStream(string path, StreamShare share, bool overwriteExisting = false, int bufferSize = 0) 548 | { 549 | // manage path 550 | var storage = GetStorageForPath(path, out string name, true); 551 | if (storage != null) 552 | return storage.CreateStream(name, share, overwriteExisting, bufferSize); 553 | 554 | // Manage already exists 555 | if (EntryExists(name)) 556 | { 557 | if (overwriteExisting) 558 | DeleteStream(name); //try to delete the old one 559 | else 560 | throw new IOException("Entry already exists!"); 561 | } 562 | 563 | //create new stream 564 | var result = _provider.CreateStream(name, StreamAccess.Write, share, bufferSize); 565 | var entry = ProviderEntryToEntry(result.EntryBase); 566 | AddToCache(entry); 567 | 568 | var ret = new StreamController(result.Stream, entry); 569 | return ret; 570 | } 571 | 572 | public Storage OpenStorage(string path, bool createIfNotExists) 573 | { 574 | try 575 | { 576 | return OpenStorage(path); 577 | } 578 | catch (StorageNotFoundException) 579 | { 580 | if (!createIfNotExists) 581 | throw; 582 | 583 | return CreateStorage(path); 584 | } 585 | } 586 | 587 | public string ReadAllText(string path) 588 | { 589 | using var stream = OpenStreamRead(path); 590 | using var sr = new StreamReader(stream); 591 | return sr.ReadToEnd(); 592 | } 593 | 594 | public string ReadAllText(string path, Encoding encoding) 595 | { 596 | using var stream = OpenStreamRead(path); 597 | using var sr = new StreamReader(stream, encoding); 598 | return sr.ReadToEnd(); 599 | } 600 | 601 | public void WriteAllText(string path, string text, Encoding encoding) 602 | { 603 | using var stream = OpenStreamWrite(path, true); 604 | using var sr = new StreamWriter(stream, encoding); 605 | sr.Write(text); 606 | } 607 | 608 | public void WriteAllText(string path, string text) 609 | { 610 | using var stream = OpenStreamWrite(path, true); 611 | using var sr = new StreamWriter(stream); 612 | sr.Write(text); 613 | } 614 | 615 | public byte[] ReadAllBytes(string path) 616 | { 617 | using var stream = OpenStreamRead(path); 618 | using var sr = new BinaryReader(stream); 619 | return sr.ReadBytes((int)stream.Length); 620 | } 621 | 622 | public void WriteAllBytes(string path, byte[] bytes) 623 | { 624 | if (bytes is null) throw new ArgumentNullException(nameof(bytes)); 625 | 626 | using var stream = OpenStreamWrite(path); 627 | stream.Write(bytes, 0, bytes.Length); 628 | } 629 | 630 | 631 | public static string PathCombine(string path1, string path2) 632 | { 633 | return System.IO.Path.Combine(path1, path2).Replace('\\', SeparatorChar); 634 | } 635 | 636 | public long GetSize() 637 | { 638 | var ret = Entries.Sum(x => x.IsStorage ? OpenStorage(x.Name).GetSize() : x.Size); 639 | return ret; 640 | } 641 | 642 | public void Copy(string sourcePath, string destinationPath, bool overwrite = false) => Copy(GetEntry(sourcePath), this, destinationPath, overwrite); 643 | public void Copy(string sourcePath, Storage destinationStorage, string destinationPath, bool overwrite = false) => Copy(GetEntry(sourcePath), destinationStorage, destinationPath, overwrite); 644 | 645 | public void CopyTo(Storage destinationStorage, string destinationPath, bool overwrite = false) 646 | { 647 | if (destinationStorage is null) throw new ArgumentNullException(nameof(destinationStorage)); 648 | 649 | CopyTo(destinationStorage.CreateStorage(destinationPath), overwrite); 650 | } 651 | 652 | public void CopyTo(Storage destinationStorage, bool overwrite = false) 653 | { 654 | foreach (var item in Entries) 655 | Copy(item.Name, destinationStorage, "", overwrite); 656 | } 657 | 658 | public static void Copy(StorageEntry srcEntry, Storage destinationStorage, string destinationPath, bool overwrite = false) 659 | { 660 | if (srcEntry is null) throw new ArgumentNullException(nameof(srcEntry)); 661 | if (destinationStorage is null) throw new ArgumentNullException(nameof(destinationStorage)); 662 | 663 | // add source filename to destination path if dest path is a folder (ended with separator) 664 | if (string.IsNullOrEmpty(System.IO.Path.GetFileName(destinationPath))) 665 | destinationPath = PathCombine(destinationPath, System.IO.Path.GetFileName(srcEntry.Name)); 666 | 667 | if (srcEntry.IsStream) 668 | { 669 | using var srcStream = srcEntry.Parent.OpenStreamRead(srcEntry.Name); 670 | using var desStream = destinationStorage.CreateStream(destinationPath, overwrite); 671 | srcStream.CopyTo(desStream); 672 | } 673 | else 674 | { 675 | var storage = srcEntry.Parent.OpenStorage(srcEntry.Name); 676 | storage.CopyTo(destinationStorage, destinationPath, overwrite); 677 | } 678 | } 679 | 680 | public static string WildcardToRegex(string pattern) 681 | { 682 | return "^" + Regex.Escape(pattern) 683 | .Replace(@"\*", ".*", StringComparison.OrdinalIgnoreCase) 684 | .Replace(@"\?", ".", StringComparison.OrdinalIgnoreCase) 685 | + "$"; 686 | } 687 | 688 | private bool _disposedValue = false; // To detect redundant calls 689 | public void Close() 690 | { 691 | lock (_lockObject) 692 | { 693 | if (_disposedValue) 694 | return; 695 | 696 | //close all substorage 697 | foreach (var storage in _storageCache) 698 | storage.Value.Close(); 699 | 700 | //close provider 701 | if (!_leaveProviderOpen) 702 | _provider.Dispose(); 703 | 704 | ClearCache(); 705 | _disposedValue = true; 706 | } 707 | } 708 | 709 | public void Delete() 710 | { 711 | if (IsRoot) 712 | throw new InvalidOperationException("Can not delete the root storage!"); 713 | Parent.DeleteStorage(Name); 714 | } 715 | 716 | private StorageEntry[] StorageEntryFromStorageEntryProvider(StorageEntryBase[] storageProviderEntry) 717 | { 718 | var entries = new List<StorageEntry>(storageProviderEntry.Length); 719 | foreach (var entryProvider in storageProviderEntry) 720 | entries.Add(ProviderEntryToEntry(entryProvider)); 721 | return entries.ToArray(); 722 | } 723 | 724 | private StorageEntry ProviderEntryToEntry(StorageEntryBase storageProviderEntry) 725 | { 726 | var isVirtualStorage = VirtualStorageProviders.TryGetValue(System.IO.Path.GetExtension(storageProviderEntry.Name), out _) || IsVirtual; 727 | var entry = new StorageEntry() 728 | { 729 | Attributes = storageProviderEntry.Attributes, 730 | IsVirtualStorage = isVirtualStorage, 731 | IsStorage = storageProviderEntry.IsStorage || isVirtualStorage, 732 | IsStream = !storageProviderEntry.IsStorage, 733 | LastWriteTime = storageProviderEntry.LastWriteTime, 734 | Name = storageProviderEntry.Name, 735 | Size = storageProviderEntry.Size, 736 | Uri = storageProviderEntry.Uri, 737 | Parent = this, 738 | Path = PathCombine(Path, storageProviderEntry.Name) 739 | }; 740 | return entry; 741 | } 742 | } 743 | } 744 | --------------------------------------------------------------------------------