Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -157,29 +157,61 @@ 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";
// 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()
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 @@ -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> {
Expand Down Expand Up @@ -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> {
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