Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
28ff7cf
Add folder upload support
tobihagemann Feb 19, 2026
dac8dff
Add upload service with foreground execution and resume support
tobihagemann Feb 19, 2026
9abeabc
Fix race conditions, error handling, and plurals in upload service
tobihagemann Feb 19, 2026
9f36ab9
Clean up upload service: simplify DAO, remove dead code, fix dialog s…
tobihagemann Feb 19, 2026
62a5063
Fix multi-resume checkpoint loss, simplify dialog and presenter code
tobihagemann Feb 19, 2026
414d878
Fix resume navigation and broaden folder resume exception handling
tobihagemann Feb 19, 2026
9c1ca9f
Add tests for UploadFolderStructure and StreamHelper
tobihagemann Feb 19, 2026
890a78d
Suspend vault auto-lock during upload picker and service flows
tobihagemann Feb 20, 2026
44a6a76
Align upload feature with app architecture patterns
tobihagemann Feb 21, 2026
fff3b4e
Migrate changed-file upload to foreground service with temp file cleanup
tobihagemann Feb 21, 2026
e1c1ad4
Migrate text editor save to foreground upload service
tobihagemann Feb 22, 2026
ebcef06
Queue concurrent uploads and handle dispatch failures
tobihagemann Feb 22, 2026
5267dcf
Fix suspended lock leak, temp file leak, and asymmetric test coverage
tobihagemann Feb 22, 2026
4e6a3c4
Fix lock suspension leaks in presenter destroyed() and upload resume …
tobihagemann Feb 22, 2026
82f5e04
Replace boolean lock suspension with time-bounded keep-alive leases
tobihagemann Feb 22, 2026
d871bf8
Fix picker lease leaks and clean up stale checkpoints for abandoned u…
tobihagemann Feb 22, 2026
552b901
Add targeted UI updates via UploadUiUpdates event bus during upload
tobihagemann Feb 22, 2026
27eaf97
Show replace dialog for folder uploads and persist choice for resume
tobihagemann Feb 23, 2026
107cf4c
Fix cancel race, stale checkpoint, and service linger in UploadService
tobihagemann Feb 23, 2026
dbf0f20
Show upload status indicator in vault list with animated fill-up icon
tobihagemann Feb 23, 2026
2f0e558
Prevent manual vault lock while upload is in progress
tobihagemann Feb 23, 2026
876ee59
Replace vault lock toast with Cancel & Lock confirmation dialog
tobihagemann Feb 23, 2026
8233d64
Add per-vault upload cancellation so locking one vault does not abort…
tobihagemann Feb 23, 2026
49b50f5
Fix stale checkpoint on pre-cancel and persist URI permissions for fi…
tobihagemann Feb 24, 2026
320e957
Fix file resume to respect checkpoint replacing flag instead of hardc…
tobihagemann Feb 24, 2026
bf20294
Include exception in Timber debug log for URI permission failure
tobihagemann Feb 24, 2026
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 @@ -8,8 +8,6 @@
import org.cryptomator.generator.Parameter;
import org.cryptomator.generator.UseCase;

import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

Expand All @@ -32,18 +30,10 @@ public List<CloudFile> execute(ProgressAware<DownloadState> progressAware) throw
cloudContentRepository.read(file.getDownloadFile(), null, file.getDataSink(), progressAware);
downloadedFiles.add(file.getDownloadFile());
} finally {
closeQuietly(file.getDataSink());
StreamHelper.closeQuietly(file.getDataSink());
}
}
return downloadedFiles;
}

private void closeQuietly(Closeable closeable) {
try {
closeable.close();
} catch (IOException e) {
// ignore
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package org.cryptomator.domain.usecases.cloud;

import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

class StreamHelper {

private static final int EOF = -1;
private static final int BUFFER_SIZE = 4096;

private StreamHelper() {
}

static void copy(InputStream in, OutputStream out) throws IOException {
byte[] buffer = new byte[BUFFER_SIZE];
try {
int read;
while ((read = in.read(buffer)) != EOF) {
out.write(buffer, 0, read);
}
} finally {
closeQuietly(in);
closeQuietly(out);
}
}

static void closeQuietly(Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (IOException e) {
// ignore
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import org.cryptomator.generator.Parameter;
import org.cryptomator.generator.UseCase;

import java.io.Closeable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
Expand All @@ -26,8 +25,6 @@
@UseCase
class UploadFiles {

private static final int EOF = -1;

private final Context context;
private final CloudContentRepository cloudContentRepository;
private final CloudFolder parent;
Expand Down Expand Up @@ -104,7 +101,7 @@ private File copyDataToFile(DataSource dataSource) {
File target = createTempFile("upload", "tmp", dir);
InputStream in = CancelAwareDataSource.wrap(dataSource, cancelledFlag).open(context);
OutputStream out = new FileOutputStream(target);
copy(in, out);
StreamHelper.copy(in, out);
dataSource.modifiedDate(context).ifPresent(value -> target.setLastModified(value.getTime()));
return target;
} catch (IOException e) {
Expand All @@ -123,36 +120,4 @@ private CloudFile writeCloudFile(String fileName, CancelAwareDataSource dataSour
size);
}

private void copy(InputStream in, OutputStream out) throws IOException {
byte[] buffer = new byte[4096];
try {
while (copyDidNotReachEof(in, out, buffer)) {
// empty
}
} finally {
closeQuietly(in);
closeQuietly(out);
}
}

private boolean copyDidNotReachEof(InputStream in, OutputStream out, byte[] buffer) throws IOException {
int read = in.read(buffer);
if (read == EOF) {
return false;
} else {
out.write(buffer, 0, read);
return true;
}
}

private void closeQuietly(Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (IOException e) {
// ignore
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package org.cryptomator.domain.usecases.cloud;

import android.content.Context;

import org.cryptomator.domain.CloudFile;
import org.cryptomator.domain.CloudFolder;
import org.cryptomator.domain.exception.BackendException;
import org.cryptomator.domain.exception.CancellationException;
import org.cryptomator.domain.exception.FatalBackendException;
import org.cryptomator.domain.repository.CloudContentRepository;
import org.cryptomator.domain.usecases.ProgressAware;
import org.cryptomator.generator.Parameter;
import org.cryptomator.generator.UseCase;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;

import static java.io.File.createTempFile;

@UseCase
class UploadFolderFiles {

private final Context context;
private final CloudContentRepository cloudContentRepository;
private final CloudFolder parent;
private final UploadFolderStructure folderStructure;

private volatile boolean cancelled;
private final Flag cancelledFlag = new Flag() {
@Override
public boolean get() {
return cancelled;
}
};

public UploadFolderFiles(Context context, //
CloudContentRepository cloudContentRepository, //
@Parameter CloudFolder parent, //
@Parameter UploadFolderStructure folderStructure) {
this.context = context;
this.cloudContentRepository = cloudContentRepository;
this.parent = parent;
this.folderStructure = folderStructure;
}

public void onCancel() {
cancelled = true;
}

public List<CloudFile> execute(ProgressAware<UploadState> progressAware) throws BackendException {
cancelled = false;
try {
return uploadFolder(parent, folderStructure, progressAware);
} catch (BackendException | RuntimeException e) {
if (cancelled) {
throw new CancellationException(e);
} else {
throw e;
}
}
}

private List<CloudFile> uploadFolder(CloudFolder targetParent, UploadFolderStructure structure, ProgressAware<UploadState> progressAware) throws BackendException {
CloudFolder createdFolder = cloudContentRepository.create( //
cloudContentRepository.folder(targetParent, structure.getFolderName()));

List<CloudFile> uploadedFiles = new ArrayList<>();

for (UploadFile file : structure.getFiles()) {
uploadedFiles.add(upload(createdFolder, file, progressAware));
}

for (UploadFolderStructure subfolder : structure.getSubfolders()) {
uploadedFiles.addAll(uploadFolder(createdFolder, subfolder, progressAware));
}

return uploadedFiles;
}

private CloudFile upload(CloudFolder folder, UploadFile uploadFile, ProgressAware<UploadState> progressAware) throws BackendException {
DataSource dataSource = uploadFile.getDataSource();
if (dataSource.size(context) != null) {
return upload(folder, uploadFile, dataSource, progressAware);
} else {
File file = copyDataToFile(dataSource);
try {
return upload(folder, uploadFile, FileBasedDataSource.from(file), progressAware);
} finally {
file.delete();
}
}
}

private CloudFile upload(CloudFolder folder, UploadFile uploadFile, DataSource dataSource, ProgressAware<UploadState> progressAware) throws BackendException {
return writeCloudFile( //
folder, //
uploadFile.getFileName(), //
CancelAwareDataSource.wrap(dataSource, cancelledFlag), //
uploadFile.getReplacing(), //
progressAware);
}

private CloudFile writeCloudFile(CloudFolder folder, String fileName, CancelAwareDataSource dataSource, boolean replacing, ProgressAware<UploadState> progressAware) throws BackendException {
Long size = dataSource.size(context);
CloudFile source = cloudContentRepository.file(folder, fileName, size);
return cloudContentRepository.write( //
source, //
dataSource, //
progressAware, //
replacing, //
size);
}

private File copyDataToFile(DataSource dataSource) {
File dir = context.getCacheDir();
try {
File target = createTempFile("upload", "tmp", dir);
InputStream in = CancelAwareDataSource.wrap(dataSource, cancelledFlag).open(context);
OutputStream out = new FileOutputStream(target);
StreamHelper.copy(in, out);
dataSource.modifiedDate(context).ifPresent(value -> target.setLastModified(value.getTime()));
return target;
} catch (IOException e) {
throw new FatalBackendException(e);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package org.cryptomator.domain.usecases.cloud;

import java.util.ArrayList;
import java.util.List;

public class UploadFolderStructure {

private final String folderName;
private final List<UploadFile> files;
private final List<UploadFolderStructure> subfolders;

public UploadFolderStructure(String folderName) {
this.folderName = folderName;
this.files = new ArrayList<>();
this.subfolders = new ArrayList<>();
}

public String getFolderName() {
return folderName;
}

public List<UploadFile> getFiles() {
return files;
}

public List<UploadFolderStructure> getSubfolders() {
return subfolders;
}

public void addFile(UploadFile file) {
this.files.add(file);
}

public void addSubfolder(UploadFolderStructure subfolder) {
this.subfolders.add(subfolder);
}

public int totalFileCount() {
int count = files.size();
for (UploadFolderStructure subfolder : subfolders) {
count += subfolder.totalFileCount();
}
return count;
}
}
Loading