Browse Source

Standardize the behavior of CreateFile/CreateFolder across different OS (#20850)

* Truncate the content when writing

To maintain consistency with other operating system APIs, see: https://github.com/AvaloniaUI/Avalonia/pull/20804 & https://github.com/AvaloniaUI/Avalonia/pull/20807

* Prevent accidental truncation when creating files

* Update comments

* Validate existing item type when creating storage

* Truncate files on creation in storage items

---------

Co-authored-by: Julien Lebosquain <julien@lebosquain.net>
release/latest
Rowe Wilson Frederisk Holme 5 months ago
committed by Julien Lebosquain
parent
commit
e121481a23
  1. 50
      src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs
  2. 12
      src/Avalonia.Base/Platform/Storage/IStorageFolder.cs
  3. 1
      src/Avalonia.Base/Platform/Storage/IStorageItem.cs
  4. 9
      src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts
  5. 2
      src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs

50
src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs

@ -157,29 +157,61 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder
{
}
public Task<IStorageFile?> CreateFileAsync(string name)
public async Task<IStorageFile?> CreateFileAsync(string name)
{
var mimeType = MimeTypeMap.Singleton?.GetMimeTypeFromExtension(MimeTypeMap.GetFileExtensionFromUrl(name)) ?? "application/octet-stream";
// Try to return an existing file to avoid creating file (1).
var existingItem = await GetItemAsync(name, false);
if (existingItem != null)
{
if (existingItem is IStorageFile existingFile)
{
// The file should be truncated when it is created.
using (var _ = await existingFile.OpenWriteAsync()) { }
return existingFile;
}
else if (existingItem is IStorageFolder)
{
// There is an item with the same name but it's not a file. We can't create a file in this case.
throw new IOException($"Can not create '{name}' because a directory with the same name already exists.");
}
}
// Create new one and return it.
var treeUri = GetTreeUri().treeUri;
var newFile = CreateDocument(Activity.ContentResolver!, treeUri!, mimeType, name);
if (newFile == null)
var mimeType = MimeTypeMap.Singleton?.GetMimeTypeFromExtension(MimeTypeMap.GetFileExtensionFromUrl(name)) ?? "application/octet-stream";
var newFile = DocumentsContract.CreateDocument(Activity.ContentResolver!, treeUri!, mimeType, name);
if(newFile == null)
{
return Task.FromResult<IStorageFile?>(null);
return null;
}
return Task.FromResult<IStorageFile?>(new AndroidStorageFile(Activity, newFile, this));
return new AndroidStorageFile(Activity, newFile, this);
}
public Task<IStorageFolder?> CreateFolderAsync(string name)
public async Task<IStorageFolder?> CreateFolderAsync(string name)
{
// Try to return an existing folder to avoid creating folder (1).
var existingItem = await GetItemAsync(name, true);
if (existingItem != null)
{
if (existingItem is IStorageFolder existingFolder)
{
return existingFolder;
}
else if (existingItem is IStorageFile)
{
// There is an item with the same name but it's not a folder. We can't create a folder in this case.
throw new IOException($"Can not create '{name}' because a file with the same name already exists.");
}
}
// Create new one and return it.
var treeUri = GetTreeUri().treeUri;
var newFolder = CreateDocument(Activity.ContentResolver!, treeUri!, Document.MimeTypeDir, name);
if (newFolder == null)
{
return Task.FromResult<IStorageFolder?>(null);
return null;
}
return Task.FromResult<IStorageFolder?>(new AndroidStorageFolder(Activity, newFolder, false, this, PermissionRoot));
return new AndroidStorageFolder(Activity, newFolder, false, this, PermissionRoot);
}
public override async Task DeleteAsync()

12
src/Avalonia.Base/Platform/Storage/IStorageFolder.cs

@ -37,16 +37,20 @@ public interface IStorageFolder : IStorageItem
Task<IStorageFile?> GetFileAsync(string name);
/// <summary>
/// Creates a file with specified name as a child of the current storage folder
/// Creates, or truncates and overwrites, a file with specified name as a child of the current storage folder.
/// </summary>
/// <param name="name">The display name</param>
/// <returns>A new <see cref="IStorageFile"/> pointing to the moved file. If not null, the current storage item becomes invalid</returns>
/// <returns>
/// A <see cref="IStorageFile"/> that provides read/write access to the file specified in <c>name</c>.
/// </returns>
Task<IStorageFile?> CreateFileAsync(string name);
/// <summary>
/// Creates a folder with specified name as a child of the current storage folder
/// Creates a folder with specified name as a child of the current storage folder unless they already exist.
/// </summary>
/// <param name="name">The display name</param>
/// <returns>A new <see cref="IStorageFolder"/> pointing to the moved file. If not null, the current storage item becomes invalid</returns>
/// <returns>
/// A <see cref="IStorageFolder"/> that represents the directory at the specified <c>name</c>. This object is returned regardless of whether a directory at the specified <c>name</c> already exists.
/// </returns>
Task<IStorageFolder?> CreateFolderAsync(string name);
}

1
src/Avalonia.Base/Platform/Storage/IStorageItem.cs

@ -1,5 +1,4 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Avalonia.Metadata;

9
src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts

@ -65,7 +65,7 @@ export class StorageItem {
await item.verityPermissions("readwrite");
return await (item.handle as FileSystemFileHandle).createWritable({ keepExistingData: true });
return await (item.handle as FileSystemFileHandle).createWritable({ keepExistingData: false });
}
public static async getProperties(item: StorageItem): Promise<{ Size: number; LastModified: number; Type: string } | null> {
@ -103,8 +103,11 @@ export class StorageItem {
}
await item.verityPermissions("readwrite");
return await ((item.handle as any).getFileHandle(name, { create: true }) as Promise<any>);
// The file should be truncated when it is created.
const fileHandle = await ((item.handle as any).getFileHandle(name, { create: true }) as Promise<any>);
const writable = await fileHandle.createWritable({ keepExistingData: false });
await writable.close();
return fileHandle;
}
public static async getFile(item: StorageItem, name: string): Promise<any | null> {

2
src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs

@ -299,7 +299,7 @@ internal sealed class IOSStorageFolder : IOSStorageItem, IStorageBookmarkFolder
var path = System.IO.Path.Combine(FilePath, name);
NSFileAttributes? attributes = null;
if (NSFileManager.DefaultManager.CreateDirectory(path, false, attributes, out var error))
if (NSFileManager.DefaultManager.CreateDirectory(path, true, attributes, out var error))
{
return Task.FromResult<IStorageFolder?>(new IOSStorageFolder(new NSUrl(path, true), SecurityScopedAncestorUrl));
}

Loading…
Cancel
Save