Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 25 additions & 11 deletions src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ protected AndroidStorageItem(Activity activity, AndroidUri uri, bool needsExtern
}

internal AndroidUri Uri { get; set; }

protected Activity Activity => _activity ?? throw new ObjectDisposedException(nameof(AndroidStorageItem));

public virtual string Name => GetColumnValue(Activity, Uri, DocumentsContract.Document.ColumnDisplayName)
Expand Down Expand Up @@ -67,7 +67,7 @@ public async Task ReleaseBookmarkAsync()

Activity.ContentResolver?.ReleasePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission);
}

public abstract Task<StorageItemProperties> GetBasicPropertiesAsync();

protected static string? GetColumnValue(Context context, AndroidUri contentUri, string column, string? selection = null, string[]? selectionArgs = null)
Expand Down Expand Up @@ -124,12 +124,12 @@ protected async Task<bool> EnsureExternalFilesPermission(bool write)

return await _activity!.CheckPermission(Manifest.Permission.ReadExternalStorage);
}

public void Dispose()
{
_activity = null;
}

internal AndroidUri? PermissionRoot => _permissionRoot;

public abstract Task DeleteAsync();
Expand All @@ -156,29 +156,43 @@ public AndroidStorageFolder(Activity activity, AndroidUri uri, bool needsExterna
{
}

public Task<IStorageFile?> CreateFileAsync(string name)
public async Task<IStorageFile?> CreateFileAsync(string name)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For CreateFileAsync i would have it truncated content. And separate method AppendFIleAsync - that will behave like CreateFileAsync, but append on top of existing data.

That will help to have retries for fetching files. and Append - for logs.

Copy link
Copy Markdown
Contributor Author

@Frederisk Frederisk Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For write actions, after #20804, when you try to call OpenWriteAsync, it will already truncate the file correctly. The question here is what happens when the Create method is called on an existing file. Just post that file; or truncate the existing file first and post it.

This can cause differences in behavior if someone tries to write some weird code:

var parentFolder = await GetParentFolderAsync();
byte[] bytes = [1, 2, 3];

// Create a file and write to it.
var file = await parentFolder.CreateFileAsync('file.txt');
var stream = await file.OpenWriteAsync();
await stream.WriteAsync(bytes);
await stream.FlushAsync();
strean.Dispose();
// Contents of file file.txt: 123

// Get and write to it.
file = await parentFolder.GetFileAsync('file.txt');
stream = await file.OpenWriteAsync();
await stream.WriteAsync(bytes);
await stream.FlushAsync();
strean.Dispose();
// Contents of file file.txt: 123

// Try creating an existing file and appending content:
file = await parentFolder.CreateFileAsync('file.txt'); // The file may or may not be truncated here, depending on the operating system.
// Contents of file file.txt: empty or 123

However, you did remind me that we do need a new method to handle file appending. AppendFileAsync seems to be confused with GetFileAsync, so perhaps IStorageFile.OpenAppendAsync or another solution would be a better choice. (Of course, that's another topic.)

{
var mimeType = MimeTypeMap.Singleton?.GetMimeTypeFromExtension(MimeTypeMap.GetFileExtensionFromUrl(name)) ?? "application/octet-stream";
var treeUri = GetTreeUri().treeUri;
var existingFile = await GetItemAsync(name, false) as IStorageFile;
if (existingFile != null)
{
// Uncommenting the following code will cause the file to be truncated when it is created:
// using var _ = await existingFile.OpenWriteAsync();
return existingFile;

}
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)
{
var treeUri = GetTreeUri().treeUri;
var existingFolder = await GetItemAsync(name, true) as IStorageFolder;
if (existingFolder != null)
{
return existingFolder;
}

var newFolder = DocumentsContract.CreateDocument(Activity.ContentResolver!, treeUri!, DocumentsContract.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()
Expand Down Expand Up @@ -332,7 +346,7 @@ public async IAsyncEnumerable<IStorageItem> GetItemsAsync()
if (fileName != name)
{
continue;
}
}

bool mineDirectory = mime == DocumentsContract.Document.MimeTypeDir;
if (isDirectory != mineDirectory)
Expand Down
12 changes: 8 additions & 4 deletions src/Avalonia.Base/Platform/Storage/IStorageFolder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 change: 0 additions & 1 deletion src/Avalonia.Base/Platform/Storage/IStorageItem.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Avalonia.Metadata;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,12 @@ export class StorageItem {
}

await item.verityPermissions("readwrite");
const fileHandle = await (item.handle as FileSystemFileHandle).createWritable({ keepExistingData: false });
// Uncommenting the following code will cause the file to be truncated when it is created:
// const writable = await fileHandle.createWritable({ keepExistingData: false });
// await writable.close();

return await (item.handle as FileSystemFileHandle).createWritable({ keepExistingData: true });
return fileHandle;
}

public static async getProperties(item: StorageItem): Promise<{ Size: number; LastModified: number; Type: string } | null> {
Expand Down
2 changes: 1 addition & 1 deletion src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ public async IAsyncEnumerable<IStorageItem> GetItemsAsync()

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));
}
Expand Down
Loading