├── 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 | False
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | true
34 | portable
35 | false
36 | bin\Debug\
37 | DEBUG;TRACE
38 | prompt
39 | 4
40 | 8.0
41 |
42 |
43 | pdbonly
44 | true
45 | bin\Release\
46 | TRACE
47 | prompt
48 | 4
49 | 8.0
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | {037613f6-867c-4b8e-b487-40da92be73bd}
66 | PortableStorage
67 |
68 |
69 |
70 |
77 |
--------------------------------------------------------------------------------
/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 | /// //** WARNING **: MUST be unique for each stream otherwise there is NO security
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 |
2 |
3 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | text/microsoft-resx
110 |
111 |
112 | 2.0
113 |
114 |
115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
119 |
120 |
121 |
122 | Resources\Test.zip;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
123 |
124 |
--------------------------------------------------------------------------------
/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 |
2 |
3 |
4 | Debug
5 | AnyCPU
6 | 8.0.30703
7 | 2.0
8 | {0B7910FF-1E1E-4A94-BFB9-A3D9039B9E14}
9 | {EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
10 | {84dd83c5-0fe3-4294-9419-09e7c8ba324f}
11 | Library
12 | Properties
13 | AndroidSample
14 | AndroidSample
15 | 512
16 | True
17 | Resources\Resource.designer.cs
18 | Resource
19 | Off
20 | false
21 | v11.0
22 | Properties\AndroidManifest.xml
23 | Resources
24 | Assets
25 | true
26 | Xamarin.Android.Net.AndroidClientHandler
27 |
28 |
29 | True
30 | portable
31 | False
32 | bin\Debug\
33 | DEBUG;TRACE
34 | prompt
35 | 4
36 | True
37 | None
38 | False
39 | false
40 | false
41 | false
42 |
43 | false
44 | 8.0
45 |
46 |
47 | PdbOnly
48 | True
49 | True
50 | bin\Release\
51 | TRACE
52 | prompt
53 | 4
54 | true
55 | False
56 | SdkOnly
57 | True
58 | false
59 | false
60 | false
61 | 8.0
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | {943961d9-7909-4028-be74-a699c21de05f}
110 | PortableStorage.Android
111 |
112 |
113 | {037613f6-867c-4b8e-b487-40da92be73bd}
114 | PortableStorage
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
129 |
--------------------------------------------------------------------------------
/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 = "")]
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(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 = "")]
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();
119 | var folders = new Dictionary();
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();
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 = "")]
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 _storageCache = new ConcurrentDictionary();
30 | private readonly ConcurrentDictionary _entryCache = new ConcurrentDictionary();
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( 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 _virtualStorageProviders;
55 | public IReadOnlyDictionary 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 | /// can include path and wildcard. eg: /folder/file.*
142 | public StorageEntry[] GetStorageEntries(string searchPattern = null)
143 | {
144 | return GetEntries(searchPattern).Where(x => x.IsStorage).ToArray();
145 | }
146 |
147 | /// can include path and wildcard. eg: /folder/file.*
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 = "")]
154 | public StorageEntry[] Entries => GetEntries(null);
155 |
156 |
157 | /// can include path and wildcard. eg: /folder/file.*
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 | ///
213 | /// return null for root storage
214 | ///
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 = "")]
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(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 |
--------------------------------------------------------------------------------