From 9856a9da02d06fa3e169948aaa4e90c8f798f3fd Mon Sep 17 00:00:00 2001 From: Michael B Date: Mon, 30 Sep 2024 23:43:36 +0300 Subject: [PATCH 1/7] implemented appender to open a file without truncating implemented in both memory and desktop, need to implement for mobile still --- internal/docs.go | 24 ++++++++++++++++ internal/docs_test.go | 40 ++++++++++++++++++++++++++ internal/driver/mobile/repository.go | 6 ++++ internal/repository/file.go | 21 ++++++++++---- internal/repository/file_test.go | 10 ++++++- internal/repository/memory.go | 12 ++++++++ internal/repository/memory_test.go | 10 ++++++- storage/repository/repository.go | 6 ++++ storage/uri.go | 42 ++++++++++++++++++++++++++++ storage/uri_test.go | 10 ++++++- 10 files changed, 173 insertions(+), 8 deletions(-) diff --git a/internal/docs.go b/internal/docs.go index 35b254591e..2fb195944e 100644 --- a/internal/docs.go +++ b/internal/docs.go @@ -115,6 +115,30 @@ func (d *Docs) Save(name string) (fyne.URIWriteCloser, error) { return storage.Writer(u) } +// Append will open a document ready for writing without truncating it's content, +// you close the returned writer for the save to complete. +// If the document for this app with that name does not exist a storage.ErrNotExists error will be returned. +func (d *Docs) Append(name string) (fyne.URIWriteCloser, error) { + if d.RootDocURI == nil { + return nil, errNoAppID + } + + u, err := storage.Child(d.RootDocURI, name) + if err != nil { + return nil, err + } + + exists, err := storage.Exists(u) + if err != nil { + return nil, err + } + if !exists { + return nil, storage.ErrNotExists + } + + return storage.Appender(u) +} + func (d *Docs) ensureRootExists() error { exists, err := storage.Exists(d.RootDocURI) if err != nil { diff --git a/internal/docs_test.go b/internal/docs_test.go index 7ca221269e..a31a7dc720 100644 --- a/internal/docs_test.go +++ b/internal/docs_test.go @@ -81,6 +81,46 @@ func TestDocs_Save(t *testing.T) { assert.Nil(t, err) } +func TestDocs_Append(t *testing.T) { + r := intRepo.NewInMemoryRepository("file") + repository.Register("file", r) + docs := &Docs{storage.NewFileURI("/tmp/docs/save")} + w, err := docs.Create("save.txt") + assert.Nil(t, err) + _, _ = w.Write([]byte{}) + _ = w.Close() + u := w.URI() + + exist, err := r.Exists(u) + assert.Nil(t, err) + assert.True(t, exist) + + w, err = docs.Save("save.txt") + assert.Nil(t, err) + n, err := w.Write([]byte("save")) + assert.Nil(t, err) + assert.Equal(t, 4, n) + err = w.Close() + assert.Nil(t, err) + + w, err = docs.Append("save.txt") + assert.Nil(t, err) + n, err = w.Write([]byte("save")) + assert.Nil(t, err) + assert.Equal(t, 4, n) + err = w.Close() + assert.Nil(t, err) + + read, err := docs.Open("save.txt") + assert.Nil(t, err) + var c []byte + n, err = read.Read(c) + assert.Nil(t, err) + assert.Equal(t, 8, n) + err = w.Close() + assert.Nil(t, err) +} + func TestDocs_Save_ErrNotExists(t *testing.T) { r := intRepo.NewInMemoryRepository("file") repository.Register("file", r) diff --git a/internal/driver/mobile/repository.go b/internal/driver/mobile/repository.go index 1e52f89b3b..8bc8eb3bb6 100644 --- a/internal/driver/mobile/repository.go +++ b/internal/driver/mobile/repository.go @@ -69,3 +69,9 @@ func (m *mobileFileRepo) Reader(u fyne.URI) (fyne.URIReadCloser, error) { func (m *mobileFileRepo) Writer(u fyne.URI) (fyne.URIWriteCloser, error) { return fileWriterForURI(u) } + +// TODO: need someone who understands native code to write this, I'm not sure +// how it works +func (m *mobileFileRepo) Appender(u fyne.URI) (fyne.URIWriteCloser, error) { + return fileWriterForURI(u) +} diff --git a/internal/repository/file.go b/internal/repository/file.go index 77e3c307ca..9f8881a638 100644 --- a/internal/repository/file.go +++ b/internal/repository/file.go @@ -72,12 +72,16 @@ func (r *FileRepository) Exists(u fyne.URI) (bool, error) { return ok, err } -func openFile(uri fyne.URI, create bool) (*file, error) { +func openFile(uri fyne.URI, write bool, truncate bool) (*file, error) { path := uri.Path() var f *os.File var err error - if create { - f, err = os.Create(path) // If it exists this will truncate which is what we wanted + if write { + if truncate { + f, err = os.Create(path) // If it exists this will truncate which is what we wanted + } else { + f, err = os.OpenFile(path, os.O_APPEND|os.O_CREATE, 0666) + } } else { f, err = os.Open(path) } @@ -88,7 +92,7 @@ func openFile(uri fyne.URI, create bool) (*file, error) { // // Since: 2.0 func (r *FileRepository) Reader(u fyne.URI) (fyne.URIReadCloser, error) { - return openFile(u, false) + return openFile(u, false, false) } // CanRead implements repository.Repository.CanRead @@ -123,7 +127,14 @@ func (r *FileRepository) Destroy(scheme string) { // // Since: 2.0 func (r *FileRepository) Writer(u fyne.URI) (fyne.URIWriteCloser, error) { - return openFile(u, true) + return openFile(u, true, true) +} + +// Appender implements repository.WritableRepository.Appender +// +// Since: 2.6 +func (r *FileRepository) Appender(u fyne.URI) (fyne.URIWriteCloser, error) { + return openFile(u, true, false) } // CanWrite implements repository.WritableRepository.CanWrite diff --git a/internal/repository/file_test.go b/internal/repository/file_test.go index 67223f3037..9f4d257664 100644 --- a/internal/repository/file_test.go +++ b/internal/repository/file_test.go @@ -187,6 +187,14 @@ func TestFileRepositoryWriter(t *testing.T) { barWriter.Close() bazWriter.Close() + bazAppender, err := storage.Appender(baz) + assert.Nil(t, err) + n, err = bazAppender.Write([]byte{1, 2, 3, 4, 5}) + assert.Nil(t, err) + assert.Equal(t, 5, n) + + bazAppender.Close() + // now make sure we can read the data back correctly fooReader, err := storage.Reader(foo) assert.Nil(t, err) @@ -203,7 +211,7 @@ func TestFileRepositoryWriter(t *testing.T) { bazReader, err := storage.Reader(baz) assert.Nil(t, err) bazData, err := io.ReadAll(bazReader) - assert.Equal(t, []byte{5, 4, 3, 2, 1, 0}, bazData) + assert.Equal(t, []byte{5, 4, 3, 2, 1, 0, 1, 2, 3, 4, 5}, bazData) assert.Nil(t, err) // close the readers, since Windows won't let us delete things with diff --git a/internal/repository/memory.go b/internal/repository/memory.go index b24234cfcd..51e7ad0f24 100644 --- a/internal/repository/memory.go +++ b/internal/repository/memory.go @@ -210,6 +210,18 @@ func (m *InMemoryRepository) Writer(u fyne.URI) (fyne.URIWriteCloser, error) { return &nodeReaderWriter{path: path, repo: m}, nil } +// Appender implements repository.WritableRepository.Appender +// +// Since: 2.6 +func (m *InMemoryRepository) Appender(u fyne.URI) (fyne.URIWriteCloser, error) { + path := u.Path() + if path == "" { + return nil, fmt.Errorf("invalid path '%s'", path) + } + + return &nodeReaderWriter{path: path, repo: m, writing: true, writeCursor: len(m.Data[path])}, nil +} + // CanWrite implements repository.WritableRepository.CanWrite // // Since: 2.0 diff --git a/internal/repository/memory_test.go b/internal/repository/memory_test.go index b61b59c1da..a55927c3b4 100644 --- a/internal/repository/memory_test.go +++ b/internal/repository/memory_test.go @@ -194,6 +194,14 @@ func TestInMemoryRepositoryWriter(t *testing.T) { barWriter.Close() bazWriter.Close() + bazAppender, err := storage.Appender(baz) + assert.Nil(t, err) + n, err = bazAppender.Write([]byte{1, 2, 3, 4, 5}) + assert.Nil(t, err) + assert.Equal(t, 5, n) + + bazAppender.Close() + // now make sure we can read the data back correctly fooReader, err := storage.Reader(foo) assert.Nil(t, err) @@ -210,7 +218,7 @@ func TestInMemoryRepositoryWriter(t *testing.T) { bazReader, err := storage.Reader(baz) assert.Nil(t, err) bazData, err := io.ReadAll(bazReader) - assert.Equal(t, []byte{5, 4, 3, 2, 1, 0}, bazData) + assert.Equal(t, []byte{5, 4, 3, 2, 1, 0, 1, 2, 3, 4, 5}, bazData) assert.Nil(t, err) // now let's test deletion diff --git a/storage/repository/repository.go b/storage/repository/repository.go index 4ef7206f10..6618f259ba 100644 --- a/storage/repository/repository.go +++ b/storage/repository/repository.go @@ -108,6 +108,12 @@ type WritableRepository interface { // Since: 2.0 Writer(u fyne.URI) (fyne.URIWriteCloser, error) + // Appender will be used to call a Writer without truncating the + // file if it exists + // + // Since: 2.6 + Appender(u fyne.URI) (fyne.URIWriteCloser, error) + // CanWrite will be used to implement calls to storage.CanWrite() for // the registered scheme of this repository. // diff --git a/storage/uri.go b/storage/uri.go index bdbe6008b3..86938864a3 100644 --- a/storage/uri.go +++ b/storage/uri.go @@ -284,6 +284,48 @@ func Writer(u fyne.URI) (fyne.URIWriteCloser, error) { return wrepo.Writer(u) } +// Appender returns URIWriteCloser set up to write to the resource that the +// URI references without truncating it first +// +// Writing to a non-extant resource should create that resource if possible +// (and if not possible, this should be reflected in the return of CanWrite()). +// Writing to an extant resource should NOT overwrite it in-place. +// +// This method can fail in several ways: +// +// - Different permissions or credentials are required to write to the +// referenced resource. +// +// - This URI scheme could represent some resources that can be +// written, but this particular URI references a resources that is +// not something that can be written. +// +// - Attempting to set up the writer depended on a lower level +// operation such as a network or filesystem access that has failed +// in some way. +// +// - If the scheme of the given URI does not have a registered +// WritableRepository instance, then this method will fail with a +// repository.ErrOperationNotSupported. +// +// Appender is backed by the repository system - this function calls into a +// scheme-specific implementation from a registered repository. +// +// Since: 2.6 +func Appender(u fyne.URI) (fyne.URIWriteCloser, error) { + repo, err := repository.ForURI(u) + if err != nil { + return nil, err + } + + wrepo, ok := repo.(repository.WritableRepository) + if !ok { + return nil, repository.ErrOperationNotSupported + } + + return wrepo.Appender(u) +} + // CanWrite determines if a given URI could be written to using the Writer() // method. It is preferred to check if a URI is writable using this method // before calling Writer(), because the underlying operations required to diff --git a/storage/uri_test.go b/storage/uri_test.go index 97146aec70..f59dc6c9e4 100644 --- a/storage/uri_test.go +++ b/storage/uri_test.go @@ -270,6 +270,14 @@ func TestWriteAndDelete(t *testing.T) { barWriter.Close() bazWriter.Close() + bazAppender, err := storage.Appender(baz) + assert.Nil(t, err) + n, err = bazAppender.Write([]byte{1, 2, 3, 4, 5}) + assert.Nil(t, err) + assert.Equal(t, 5, n) + + bazAppender.Close() + // now make sure we can read the data back correctly fooReader, err := storage.Reader(foo) assert.Nil(t, err) @@ -286,7 +294,7 @@ func TestWriteAndDelete(t *testing.T) { bazReader, err := storage.Reader(baz) assert.Nil(t, err) bazData, err := io.ReadAll(bazReader) - assert.Equal(t, []byte{5, 4, 3, 2, 1, 0}, bazData) + assert.Equal(t, []byte{5, 4, 3, 2, 1, 0, 1, 2, 3, 4, 5}, bazData) assert.Nil(t, err) // now let's test deletion From 9e868e32cca2ca70f8a813d5c012fa92b2286e59 Mon Sep 17 00:00:00 2001 From: Michael B Date: Tue, 1 Oct 2024 00:17:11 +0300 Subject: [PATCH 2/7] fixed failing tests in internal/docs because I forgot how io reader works --- internal/docs_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/docs_test.go b/internal/docs_test.go index a31a7dc720..6c9ed23430 100644 --- a/internal/docs_test.go +++ b/internal/docs_test.go @@ -1,6 +1,7 @@ package internal import ( + "io" "testing" intRepo "fyne.io/fyne/v2/internal/repository" @@ -113,9 +114,9 @@ func TestDocs_Append(t *testing.T) { read, err := docs.Open("save.txt") assert.Nil(t, err) - var c []byte + c := make([]byte, 16) n, err = read.Read(c) - assert.Nil(t, err) + assert.ErrorIs(t, err, io.EOF) assert.Equal(t, 8, n) err = w.Close() assert.Nil(t, err) From 276adfeb19da62e9ffaf3ab8fdafc379fb3ee517 Mon Sep 17 00:00:00 2001 From: Michael B Date: Tue, 1 Oct 2024 00:30:30 +0300 Subject: [PATCH 3/7] set openFile to os.O_WRONLY assuming that's why it fails? --- internal/repository/file.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/repository/file.go b/internal/repository/file.go index 9f8881a638..75cf1b66bd 100644 --- a/internal/repository/file.go +++ b/internal/repository/file.go @@ -80,7 +80,7 @@ func openFile(uri fyne.URI, write bool, truncate bool) (*file, error) { if truncate { f, err = os.Create(path) // If it exists this will truncate which is what we wanted } else { - f, err = os.OpenFile(path, os.O_APPEND|os.O_CREATE, 0666) + f, err = os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666) } } else { f, err = os.Open(path) From 2ba9612679d93361a053964dde2d11e4fcba6b09 Mon Sep 17 00:00:00 2001 From: Michael B Date: Fri, 18 Oct 2024 01:23:52 +0300 Subject: [PATCH 4/7] first draft for append on mobile drivers --- internal/driver/mobile/android.c | 9 +++++++-- internal/driver/mobile/file.go | 7 ++++--- internal/driver/mobile/file_android.go | 10 +++++----- internal/driver/mobile/file_desktop.go | 2 +- internal/driver/mobile/file_ios.go | 6 +++--- internal/driver/mobile/file_ios.m | 8 ++++++-- internal/driver/mobile/repository.go | 4 ++-- 7 files changed, 28 insertions(+), 18 deletions(-) diff --git a/internal/driver/mobile/android.c b/internal/driver/mobile/android.c index ba0ba6389d..14e72b66b8 100644 --- a/internal/driver/mobile/android.c +++ b/internal/driver/mobile/android.c @@ -144,7 +144,7 @@ void* openStream(uintptr_t jni_env, uintptr_t ctx, char* uriCstr) { return (*env)->NewGlobalRef(env, stream); } -void* saveStream(uintptr_t jni_env, uintptr_t ctx, char* uriCstr) { +void* saveStream(uintptr_t jni_env, uintptr_t ctx, char* uriCstr, bool truncate) { JNIEnv *env = (JNIEnv*)jni_env; jobject resolver = getContentResolver(jni_env, ctx); @@ -152,7 +152,12 @@ void* saveStream(uintptr_t jni_env, uintptr_t ctx, char* uriCstr) { jmethodID saveOutputStream = find_method(env, resolverClass, "openOutputStream", "(Landroid/net/Uri;Ljava/lang/String;)Ljava/io/OutputStream;"); jobject uri = parseURI(jni_env, ctx, uriCstr); - jstring modes = (*env)->NewStringUTF(env, "wt"); // truncate before write + jstring modes = NULL; + if (truncate) { + jstring modes = (*env)->NewStringUTF(env, "wt"); // truncate before write + } else { + jstring modes = (*env)->NewStringUTF(env, "wa"); + } jobject stream = (jobject)(*env)->CallObjectMethod(env, resolver, saveOutputStream, uri, modes); jthrowable loadErr = (*env)->ExceptionOccurred(env); diff --git a/internal/driver/mobile/file.go b/internal/driver/mobile/file.go index d3af9b0e57..6f3d2a47ff 100644 --- a/internal/driver/mobile/file.go +++ b/internal/driver/mobile/file.go @@ -96,9 +96,9 @@ func (f *fileSave) URI() fyne.URI { return f.uri } -func fileWriterForURI(u fyne.URI) (fyne.URIWriteCloser, error) { +func fileWriterForURI(u fyne.URI, truncate bool) (fyne.URIWriteCloser, error) { file := &fileSave{uri: u} - write, err := nativeFileSave(file) + write, err := nativeFileSave(file, truncate) if write == nil { return nil, err } @@ -126,7 +126,8 @@ func ShowFileSavePicker(callback func(fyne.URIWriteCloser, error), filter storag return } - f, err := fileWriterForURI(uri) + // TODO: does the save dialog want to truncate by default? + f, err := fileWriterForURI(uri, true) if f != nil { f.(*fileSave).done = closer } diff --git a/internal/driver/mobile/file_android.go b/internal/driver/mobile/file_android.go index 1bbf82297e..9c215084d5 100644 --- a/internal/driver/mobile/file_android.go +++ b/internal/driver/mobile/file_android.go @@ -12,7 +12,7 @@ bool deleteURI(uintptr_t jni_env, uintptr_t ctx, char* uriCstr); bool existsURI(uintptr_t jni_env, uintptr_t ctx, char* uriCstr); void* openStream(uintptr_t jni_env, uintptr_t ctx, char* uriCstr); char* readStream(uintptr_t jni_env, uintptr_t ctx, void* stream, int len, int* total); -void* saveStream(uintptr_t jni_env, uintptr_t ctx, char* uriCstr); +void* saveStream(uintptr_t jni_env, uintptr_t ctx, char* uriCstr, bool truncate); void writeStream(uintptr_t jni_env, uintptr_t ctx, void* stream, char* data, int len); void closeStream(uintptr_t jni_env, uintptr_t ctx, void* stream); */ @@ -99,13 +99,13 @@ func nativeFileOpen(f *fileOpen) (io.ReadCloser, error) { return stream, nil } -func saveStream(uri string) unsafe.Pointer { +func saveStream(uri string, truncate bool) unsafe.Pointer { uriStr := C.CString(uri) defer C.free(unsafe.Pointer(uriStr)) var stream unsafe.Pointer app.RunOnJVM(func(_, env, ctx uintptr) error { - streamPtr := C.saveStream(C.uintptr_t(env), C.uintptr_t(ctx), uriStr) + streamPtr := C.saveStream(C.uintptr_t(env), C.uintptr_t(ctx), uriStr, C.bool(truncate)) if streamPtr == C.NULL { return os.ErrNotExist } @@ -116,12 +116,12 @@ func saveStream(uri string) unsafe.Pointer { return stream } -func nativeFileSave(f *fileSave) (io.WriteCloser, error) { +func nativeFileSave(f *fileSave, truncate bool) (io.WriteCloser, error) { if f.uri == nil || f.uri.String() == "" { return nil, nil } - ret := saveStream(f.uri.String()) + ret := saveStream(f.uri.String(), truncate) if ret == nil { return nil, storage.ErrNotExists } diff --git a/internal/driver/mobile/file_desktop.go b/internal/driver/mobile/file_desktop.go index 8334d9d8e7..aa2538b9eb 100644 --- a/internal/driver/mobile/file_desktop.go +++ b/internal/driver/mobile/file_desktop.go @@ -25,7 +25,7 @@ func nativeFileOpen(*fileOpen) (io.ReadCloser, error) { return nil, nil } -func nativeFileSave(*fileSave) (io.WriteCloser, error) { +func nativeFileSave(*fileSave, bool) (io.WriteCloser, error) { // no-op as we use the internal FileRepository return nil, nil } diff --git a/internal/driver/mobile/file_ios.go b/internal/driver/mobile/file_ios.go index 10e0112b9d..bd5ee4292b 100644 --- a/internal/driver/mobile/file_ios.go +++ b/internal/driver/mobile/file_ios.go @@ -14,7 +14,7 @@ bool iosExistsPath(const char* path); void* iosParseUrl(const char* url); const void* iosReadFromURL(void* url, int* len); -const void* iosOpenFileWriter(void* url); +const void* iosOpenFileWriter(void* url, bool truncate); void iosCloseFileWriter(void* handle); const int iosWriteToFile(void* handle, const void* bytes, int len); */ @@ -136,7 +136,7 @@ func nativeFileOpen(f *fileOpen) (io.ReadCloser, error) { return fileStruct, nil } -func nativeFileSave(f *fileSave) (io.WriteCloser, error) { +func nativeFileSave(f *fileSave, truncate bool) (io.WriteCloser, error) { if f.uri == nil || f.uri.String() == "" { return nil, nil } @@ -146,7 +146,7 @@ func nativeFileSave(f *fileSave) (io.WriteCloser, error) { url := C.iosParseUrl(cStr) - handle := C.iosOpenFileWriter(url) + handle := C.iosOpenFileWriter(url, C.bool(truncate)) fileStruct := &secureWriteCloser{handle: handle, closer: f.done} return fileStruct, nil } diff --git a/internal/driver/mobile/file_ios.m b/internal/driver/mobile/file_ios.m index 3bf743236a..0aefdbc2e9 100644 --- a/internal/driver/mobile/file_ios.m +++ b/internal/driver/mobile/file_ios.m @@ -27,9 +27,13 @@ bool iosExistsPath(const char* path) { return data.bytes; } -const void* iosOpenFileWriter(void* urlPtr) { +const void* iosOpenFileWriter(void* urlPtr, bool truncate) { NSURL* url = (NSURL*)urlPtr; - [[NSFileManager defaultManager] createFileAtPath:url.path contents:nil attributes:nil]; + NSData* data = nil; + if (!truncate) { + data = [NSData dataWithContentsOfURL:url]; + } + [[NSFileManager defaultManager] createFileAtPath:url.path contents:data attributes:nil]; NSError *err = nil; // TODO handle error diff --git a/internal/driver/mobile/repository.go b/internal/driver/mobile/repository.go index 8bc8eb3bb6..d8d5088123 100644 --- a/internal/driver/mobile/repository.go +++ b/internal/driver/mobile/repository.go @@ -67,11 +67,11 @@ func (m *mobileFileRepo) Reader(u fyne.URI) (fyne.URIReadCloser, error) { } func (m *mobileFileRepo) Writer(u fyne.URI) (fyne.URIWriteCloser, error) { - return fileWriterForURI(u) + return fileWriterForURI(u, true) } // TODO: need someone who understands native code to write this, I'm not sure // how it works func (m *mobileFileRepo) Appender(u fyne.URI) (fyne.URIWriteCloser, error) { - return fileWriterForURI(u) + return fileWriterForURI(u, false) } From eeb851157789085ee2b8a64a3c12e684f2102b24 Mon Sep 17 00:00:00 2001 From: Michael B Date: Sun, 20 Oct 2024 15:23:22 +0300 Subject: [PATCH 5/7] made real ios append without loading entire file to memory thanks to https://lists.apple.com/archives/cocoa-dev/2002/Aug/msg00754.html for giving me the necessary shape for the function --- internal/driver/mobile/file_ios.m | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/internal/driver/mobile/file_ios.m b/internal/driver/mobile/file_ios.m index 0aefdbc2e9..18b08de240 100644 --- a/internal/driver/mobile/file_ios.m +++ b/internal/driver/mobile/file_ios.m @@ -29,15 +29,20 @@ bool iosExistsPath(const char* path) { const void* iosOpenFileWriter(void* urlPtr, bool truncate) { NSURL* url = (NSURL*)urlPtr; - NSData* data = nil; - if (!truncate) { - data = [NSData dataWithContentsOfURL:url]; + + if (truncate || ![[NSFileManager defaultManager] fileExistsAtPath:url.path]) { + [[NSFileManager defaultManager] createFileAtPath:url.path contents:nil attributes:nil]; } - [[NSFileManager defaultManager] createFileAtPath:url.path contents:data attributes:nil]; NSError *err = nil; // TODO handle error - return [NSFileHandle fileHandleForWritingToURL:url error:&err]; + NSFileHandle* handle = [NSFileHandle fileHandleForWritingToURL:url error:&err]; + + if (!truncate) { + [handle truncateFileAtOffset:[handle seekToEndOfFile]]; + } + + return handle; } void iosCloseFileWriter(void* handlePtr) { From ed1fd11d79096fba0c1f22b45d2dddc3b110e9bb Mon Sep 17 00:00:00 2001 From: Michael B Date: Sat, 2 Nov 2024 23:50:55 +0200 Subject: [PATCH 6/7] added optional interface AppendableRepository extending WritableRepository --- internal/driver/mobile/repository.go | 3 +-- internal/repository/file.go | 3 ++- internal/repository/memory.go | 3 ++- storage/repository/repository.go | 20 ++++++++++++++------ storage/uri.go | 4 ++-- 5 files changed, 21 insertions(+), 12 deletions(-) diff --git a/internal/driver/mobile/repository.go b/internal/driver/mobile/repository.go index d8d5088123..0b271f560d 100644 --- a/internal/driver/mobile/repository.go +++ b/internal/driver/mobile/repository.go @@ -11,6 +11,7 @@ var _ repository.Repository = (*mobileFileRepo)(nil) var _ repository.HierarchicalRepository = (*mobileFileRepo)(nil) var _ repository.ListableRepository = (*mobileFileRepo)(nil) var _ repository.WritableRepository = (*mobileFileRepo)(nil) +var _ repository.AppendableRepository = (*mobileFileRepo)(nil) type mobileFileRepo struct { } @@ -70,8 +71,6 @@ func (m *mobileFileRepo) Writer(u fyne.URI) (fyne.URIWriteCloser, error) { return fileWriterForURI(u, true) } -// TODO: need someone who understands native code to write this, I'm not sure -// how it works func (m *mobileFileRepo) Appender(u fyne.URI) (fyne.URIWriteCloser, error) { return fileWriterForURI(u, false) } diff --git a/internal/repository/file.go b/internal/repository/file.go index 75cf1b66bd..985b8a4df9 100644 --- a/internal/repository/file.go +++ b/internal/repository/file.go @@ -19,6 +19,7 @@ const fileSchemePrefix string = "file://" // declare conformance with repository types var _ repository.Repository = (*FileRepository)(nil) var _ repository.WritableRepository = (*FileRepository)(nil) +var _ repository.AppendableRepository = (*FileRepository)(nil) var _ repository.HierarchicalRepository = (*FileRepository)(nil) var _ repository.ListableRepository = (*FileRepository)(nil) var _ repository.MovableRepository = (*FileRepository)(nil) @@ -130,7 +131,7 @@ func (r *FileRepository) Writer(u fyne.URI) (fyne.URIWriteCloser, error) { return openFile(u, true, true) } -// Appender implements repository.WritableRepository.Appender +// Appender implements repository.AppendableRepository.Appender // // Since: 2.6 func (r *FileRepository) Appender(u fyne.URI) (fyne.URIWriteCloser, error) { diff --git a/internal/repository/memory.go b/internal/repository/memory.go index 51e7ad0f24..c401f80334 100644 --- a/internal/repository/memory.go +++ b/internal/repository/memory.go @@ -19,6 +19,7 @@ var _ fyne.URIWriteCloser = (*nodeReaderWriter)(nil) // declare conformance with repository types var _ repository.Repository = (*InMemoryRepository)(nil) var _ repository.WritableRepository = (*InMemoryRepository)(nil) +var _ repository.AppendableRepository = (*InMemoryRepository)(nil) var _ repository.HierarchicalRepository = (*InMemoryRepository)(nil) var _ repository.CopyableRepository = (*InMemoryRepository)(nil) var _ repository.MovableRepository = (*InMemoryRepository)(nil) @@ -210,7 +211,7 @@ func (m *InMemoryRepository) Writer(u fyne.URI) (fyne.URIWriteCloser, error) { return &nodeReaderWriter{path: path, repo: m}, nil } -// Appender implements repository.WritableRepository.Appender +// Appender implements repository.AppendableRepository.Appender // // Since: 2.6 func (m *InMemoryRepository) Appender(u fyne.URI) (fyne.URIWriteCloser, error) { diff --git a/storage/repository/repository.go b/storage/repository/repository.go index 6618f259ba..08357e7c29 100644 --- a/storage/repository/repository.go +++ b/storage/repository/repository.go @@ -108,12 +108,6 @@ type WritableRepository interface { // Since: 2.0 Writer(u fyne.URI) (fyne.URIWriteCloser, error) - // Appender will be used to call a Writer without truncating the - // file if it exists - // - // Since: 2.6 - Appender(u fyne.URI) (fyne.URIWriteCloser, error) - // CanWrite will be used to implement calls to storage.CanWrite() for // the registered scheme of this repository. // @@ -127,6 +121,20 @@ type WritableRepository interface { Delete(u fyne.URI) error } +// AppendableRepository is an extension of the WritableRepository interface which also +// supports opening a writer for URIs in append mode, without truncating their contents +// +// Since: 2.0 +type AppendableRepository interface { + WritableRepository + + // Appender will be used to call a Writer without truncating the + // file if it exists + // + // Since: 2.6 + Appender(u fyne.URI) (fyne.URIWriteCloser, error) +} + // ListableRepository is an extension of the Repository interface which also // supports obtaining directory listings (generally analogous to a directory // listing) for URIs of the scheme it is registered to. diff --git a/storage/uri.go b/storage/uri.go index 86938864a3..2230548c84 100644 --- a/storage/uri.go +++ b/storage/uri.go @@ -305,7 +305,7 @@ func Writer(u fyne.URI) (fyne.URIWriteCloser, error) { // in some way. // // - If the scheme of the given URI does not have a registered -// WritableRepository instance, then this method will fail with a +// AppendableRepository instance, then this method will fail with a // repository.ErrOperationNotSupported. // // Appender is backed by the repository system - this function calls into a @@ -318,7 +318,7 @@ func Appender(u fyne.URI) (fyne.URIWriteCloser, error) { return nil, err } - wrepo, ok := repo.(repository.WritableRepository) + wrepo, ok := repo.(repository.AppendableRepository) if !ok { return nil, repository.ErrOperationNotSupported } From 504b502d607ccfe953f0e906b44103e609ddf691 Mon Sep 17 00:00:00 2001 From: Michael B Date: Sun, 3 Nov 2024 00:07:22 +0200 Subject: [PATCH 7/7] removed Append method from internal.Docs --- internal/docs.go | 24 ------------------------ internal/docs_test.go | 41 ----------------------------------------- 2 files changed, 65 deletions(-) diff --git a/internal/docs.go b/internal/docs.go index 2fb195944e..35b254591e 100644 --- a/internal/docs.go +++ b/internal/docs.go @@ -115,30 +115,6 @@ func (d *Docs) Save(name string) (fyne.URIWriteCloser, error) { return storage.Writer(u) } -// Append will open a document ready for writing without truncating it's content, -// you close the returned writer for the save to complete. -// If the document for this app with that name does not exist a storage.ErrNotExists error will be returned. -func (d *Docs) Append(name string) (fyne.URIWriteCloser, error) { - if d.RootDocURI == nil { - return nil, errNoAppID - } - - u, err := storage.Child(d.RootDocURI, name) - if err != nil { - return nil, err - } - - exists, err := storage.Exists(u) - if err != nil { - return nil, err - } - if !exists { - return nil, storage.ErrNotExists - } - - return storage.Appender(u) -} - func (d *Docs) ensureRootExists() error { exists, err := storage.Exists(d.RootDocURI) if err != nil { diff --git a/internal/docs_test.go b/internal/docs_test.go index 6c9ed23430..7ca221269e 100644 --- a/internal/docs_test.go +++ b/internal/docs_test.go @@ -1,7 +1,6 @@ package internal import ( - "io" "testing" intRepo "fyne.io/fyne/v2/internal/repository" @@ -82,46 +81,6 @@ func TestDocs_Save(t *testing.T) { assert.Nil(t, err) } -func TestDocs_Append(t *testing.T) { - r := intRepo.NewInMemoryRepository("file") - repository.Register("file", r) - docs := &Docs{storage.NewFileURI("/tmp/docs/save")} - w, err := docs.Create("save.txt") - assert.Nil(t, err) - _, _ = w.Write([]byte{}) - _ = w.Close() - u := w.URI() - - exist, err := r.Exists(u) - assert.Nil(t, err) - assert.True(t, exist) - - w, err = docs.Save("save.txt") - assert.Nil(t, err) - n, err := w.Write([]byte("save")) - assert.Nil(t, err) - assert.Equal(t, 4, n) - err = w.Close() - assert.Nil(t, err) - - w, err = docs.Append("save.txt") - assert.Nil(t, err) - n, err = w.Write([]byte("save")) - assert.Nil(t, err) - assert.Equal(t, 4, n) - err = w.Close() - assert.Nil(t, err) - - read, err := docs.Open("save.txt") - assert.Nil(t, err) - c := make([]byte, 16) - n, err = read.Read(c) - assert.ErrorIs(t, err, io.EOF) - assert.Equal(t, 8, n) - err = w.Close() - assert.Nil(t, err) -} - func TestDocs_Save_ErrNotExists(t *testing.T) { r := intRepo.NewInMemoryRepository("file") repository.Register("file", r)