Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upload folder support #4859

Closed
wants to merge 7 commits into from
Closed

Conversation

kilves
Copy link

@kilves kilves commented Nov 19, 2019

#4505 develeper stopped updating his PR, so here's an alternate solution that should implement the requirements mentioned in old PR.

Fixes #184

@kilves kilves force-pushed the upload-folder-support branch from cc2a9f4 to a5803c2 Compare November 20, 2019 17:40
@codecov
Copy link

codecov bot commented Nov 20, 2019

Codecov Report

Merging #4859 into master will decrease coverage by 0.13%.
The diff coverage is 1.34%.

@@             Coverage Diff              @@
##             master    #4859      +/-   ##
============================================
- Coverage     17.71%   17.58%   -0.14%     
  Complexity        3        3              
============================================
  Files           384      383       -1     
  Lines         32650    32624      -26     
  Branches       4597     4611      +14     
============================================
- Hits           5785     5736      -49     
- Misses        25942    25974      +32     
+ Partials        923      914       -9
Impacted Files Coverage Δ Complexity Δ
...oud/android/ui/fragment/LocalFileListFragment.java 0% <0%> (ø) 0 <0> (ø) ⬇️
...cloud/android/ui/activity/UploadFilesActivity.java 0% <0%> (ø) 0 <0> (ø) ⬇️
...android/ui/asynctasks/CheckAvailableSpaceTask.java 0% <0%> (ø) 0 <0> (ø) ⬇️
...cloud/android/ui/adapter/LocalFileListAdapter.java 0% <0%> (ø) 0 <0> (ø) ⬇️
...cloud/android/ui/activity/FileDisplayActivity.java 19.11% <2.17%> (-0.98%) 0 <0> (ø)
...ava/com/nextcloud/client/account/RegisteredUser.kt 60% <0%> (-40%) 0% <0%> (ø)
...rc/main/java/com/nextcloud/java/util/Optional.java 0% <0%> (-17.31%) 0% <0%> (ø)
...oud/android/ui/activity/SyncedFoldersActivity.java 23.39% <0%> (-2.93%) 0% <0%> (ø)
...java/com/nextcloud/client/account/AnonymousUser.kt 75% <0%> (-2.78%) 0% <0%> (ø)
...xtcloud/client/account/UserAccountManagerImpl.java 46.76% <0%> (-2.16%) 0% <0%> (ø)
... and 43 more

@kilves kilves force-pushed the upload-folder-support branch from 7eefd80 to fc718c8 Compare November 20, 2019 18:09
@nextcloud-android-bot
Copy link
Collaborator

@kilves kilves force-pushed the upload-folder-support branch from 91a2213 to 88ecc91 Compare November 21, 2019 21:30
@nextcloud-android-bot
Copy link
Collaborator

@kilves
Copy link
Author

kilves commented Nov 22, 2019

IT test fail might be unrelated, the exact same error has happened before on a completely different PR: https://kaminsky.me/nc-dev/android-integrationTests/10775/index.html

@AndyScherzinger
Copy link
Member

hi @kilves,

first of all welcome to the Nextcloud Android client project and thank for taking an interest in it and for picking up this task ❤️

Tests are actually a bit flaky, so no worries. Can you do a rebase against the latest master please. See #4859 (comment) which shows an increase in linbt warnings (breaking the build) which might be due to the not-up-to-date master branch of yours (or needs fixing, hard to tell right now). The rebase would also remove all changes not made within this PR itself 😃

cc @tobiasKaminsky for review

@AndyScherzinger
Copy link
Member

Also cc @ezaquarii for review :)

@kilves kilves force-pushed the upload-folder-support branch from 88ecc91 to 805a236 Compare November 26, 2019 08:43
Copy link
Collaborator

@ezaquarii ezaquarii left a comment

Choose a reason for hiding this comment

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

Sorry for dry tone, but I'm finishing my train journey and wanted to wrap it up fast, fast, fast. :)

Anyway, we have quite some things to improve.

Also, would be it be possible to extract all of this logic from Activity into a separate class and put under unit test? We're suffering from logic mothballing in UI components and that puts a lot of strain on us already so we try to improve the quality rather aggressively.

Changes in upload menu:

* User is now able to select files on different folders at the same time
* Made local folders selectable (long-press to select)
* When local folder is selected, all files and subfolders are also selected
* When user presses upload, app replicates directory structure to upload target
(from the point of view of currently open local folder, or common ancestor folder if impossible)
* Before upload starts, app automatically creates necessary folders to remote

Signed-off-by: Petri <petri.hetta@dicode.fi>
Signed-off-by: Petri <petri.hetta@dicode.fi>
Signed-off-by: Petri <petri.hetta@dicode.fi>
Signed-off-by: Petri <petri.hetta@dicode.fi>
Signed-off-by: Petri <petri.hetta@dicode.fi>
Signed-off-by: Petri <petri.hetta@dicode.fi>
@kilves kilves force-pushed the upload-folder-support branch from 805a236 to 6c3088a Compare November 28, 2019 19:00
Signed-off-by: Petri <petri.hetta@dicode.fi>
@kilves kilves force-pushed the upload-folder-support branch from 6c3088a to 4f50574 Compare November 28, 2019 19:00
@nextcloud nextcloud deleted a comment from ezaquarii Nov 28, 2019
@nextcloud-android-bot
Copy link
Collaborator

APK file: https://www.kaminsky.me/nc-dev/android-artifacts/11787.apk

qrcode

To test this change/fix you can simply download above APK file and install and test it in parallel to your existing Nextcloud app.

@nextcloud-android-bot
Copy link
Collaborator

Codacy

333

Lint

TypemasterPR
Warnings5959
Errors00

SpotBugs (new)

Warning TypeNumber
Bad practice Warnings26
Correctness Warnings72
Internationalization Warnings13
Malicious code vulnerability Warnings5
Multithreaded correctness Warnings9
Performance Warnings105
Security Warnings44
Dodgy code Warnings138
Total412

SpotBugs (master)

Warning TypeNumber
Bad practice Warnings26
Correctness Warnings70
Internationalization Warnings13
Malicious code vulnerability Warnings5
Multithreaded correctness Warnings9
Performance Warnings104
Security Warnings44
Dodgy code Warnings136
Total407

SpotBugs increased!

@kilves
Copy link
Author

kilves commented Nov 28, 2019

Ok, so I did some refactoring and the code should be more readable now. I also moved the logic to separate classes. I'm not sure about unit tests in this case. The code is still largely dependent on the existence of a context, because you still need intents to create folders before uploading files. So it would really be an integration test. I can write some though if they are really necessary. I'm also open to suggestions about FilesUploader class if there's not enough decoupling for example.


requester.uploadNewFile(
fileActivity,
account,
Copy link
Collaborator

@ezaquarii ezaquarii Nov 28, 2019

Choose a reason for hiding this comment

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

Let's not create Account variables.

We're running a continuous refactoring effort where all calls to toPlatformAccount() are being chased. Because we have tons of it, it's much easier to browse the analysis report if it's called in point of use rather than calls being coalesced via temporary variables, as IDE can tell us how many items we have in each class.

For the record, toPlaformAccount() is being retired as we speak/write, it just takes time.

requester.uploadNewFile(..., user.toPlaformAccount(), ...) // makes our refactoring easier


public class FilesUploader {

private FileActivity fileActivity;
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't see any Activity APIs being used, so we shall not depend on it.

intent.putExtra(OperationsService.EXTRA_ACCOUNT, user.toPlatformAccount());
intent.putExtra(OperationsService.EXTRA_REMOTE_PATH, folder);
intent.putExtra(OperationsService.EXTRA_CREATE_FULL_PATH, true);
fileActivity.getOperationsServiceBinder().queueNewOperation(intent);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Few issues here:

  1. we inject FileActivity only to call a getter.
  2. 2 statements per line

We shall inject the OperationsServiceBinder instead.

String[] remotePaths = getRemoteTargetPaths(remoteTargets).toArray(new String[]{});

requester.uploadNewFile(
fileActivity,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Context is needed here.

@@ -286,6 +288,7 @@ protected void onCreate(Bundle savedInstanceState) {
}

mPlayerConnection = new PlayerServiceConnection(this);
filesUploader = new FilesUploader(this);
Copy link
Collaborator

@ezaquarii ezaquarii Nov 28, 2019

Choose a reason for hiding this comment

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

Let's constructor-inject all dependencies:

  1. Context from getApplicationContext()
  2. OperationsServiceBinder

* @param user current user
* @return true if this operation was consumed, false otherwise
*/
public boolean onCreateFolderOperationFinish(CreateFolderOperation operation,
Copy link
Collaborator

Choose a reason for hiding this comment

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

This name (and I know it's a delegate from legacy code, so no blames) suggests that we're creating some mysterious folder operation finish (a finish event?).

JavaDoc tells that it's onFinish<what/noun> callback.

Let's rename both methods.

false
);
private void uploadFromFileSystem(List<File> files, int resultCode) {
filesUploader.uploadFromFileSystem(files, resultCode, null, getCurrentDir(),
Copy link
Collaborator

Choose a reason for hiding this comment

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

When args list is too long, we make it vertical to boost readability.

break;
}
String replicationStartDir = data.getStringExtra(UploadFilesActivity.EXTRA_REPLICATION_START_DIR);
filesUploader.uploadFromFileSystem(files, resultCode, replicationStartDir, getCurrentDir(),
Copy link
Collaborator

Choose a reason for hiding this comment

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

When args list is too long, we make it vertical to boost readability.

String targetPath = targetDirectory.getAbsolutePath();
String currentPath = currentDirectory.getAbsolutePath();
if (targetPath.startsWith(currentPath)) {
return new File(targetPath.replace(targetPath.replace(currentPath, ""), ""));
Copy link
Collaborator

Choose a reason for hiding this comment

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

It's generally bad practice to have more than 1 statement per line.
We have 3 here.

return false;
}

private List<UploadTarget> getRemoteTargets(@NonNull List<File> localFiles,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Javadoc pls

@@ -41,6 +43,15 @@ public CheckAvailableSpaceTask(CheckAvailableSpaceListener callback, String... p
this.callback = callback;
}

public CheckAvailableSpaceTask(CheckAvailableSpaceListener callback, Collection<File> files) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

It would be beneficial to kill the legacy constructor that uses String[]. Is it feasible?

private List<File> localFiles = new ArrayList<>();
private int resultCode;

public static class UploadTarget {
Copy link
Collaborator

Choose a reason for hiding this comment

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

In Kotlin, this would be 1 line data class. :)

* @param currentDir the directory user currently is in
* @param user current user
*/
public void uploadFromFileSystem(List<File> localFiles,
Copy link
Collaborator

Choose a reason for hiding this comment

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

JavaDoc tells me it's places a request.
Name tells me it uploads.

Shall we bring the function name on par with the description?

@nextcloud nextcloud deleted a comment from kilves Nov 28, 2019
@kilves
Copy link
Author

kilves commented Nov 28, 2019

@ezaquarii did you delete the long comment chain about javadocs and refactoring?

Copy link
Collaborator

@ezaquarii ezaquarii left a comment

Choose a reason for hiding this comment

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

@kilves, first of all, I'd like to thank you for the effort taken to bring this feature forward, taking stale PR, and for the refactoring you've done. Before I move forward, please know that you have choosen to work on the most challenging part of the application.

For the time being, let's ignore my inline comments that I put in the code. While I was going through the code line by line over past hours, I realized that we have some fundamental issues to be addressed before we sink more time into marginal code improvements. I'll leave those comments in case we find it useful at some point, but please ignore them for now.

So far (and please correct me if I'm wrong), this is how I see the situation:

  1. upload is done in the background by a FileUploader service (subclass of android.app.Service)
  2. the service has a sneaky backdoor in form of the FileUploader.Requester (that is in fact an intent Builder cross-bred with some sort of ServiceConnection concept)
  3. FilesUploader is a stateful wrapper around this backdoor into FileUploader
  4. FileDisplayActivity.requestUploadOfFilesFromFileSystem() is a fire-and-forget enqueue method that uses that builder to enqueue uploads into service in UI-independent way

Here are my initial concerns:

  1. We introduce new stateful FilesUploader concept that is a wrapper around a stateless Requester backdoor into the FileUploader service
  2. This FilesUploader genuinely just builds a request that is then sent to the service, so it's more of a Builder kind of thing again...
  3. ...except that this time the request is partial and continuation is driven by callbacks and we changed queue push operation (requestUploadOfFilesFromFileSystem) into a state machine living in the UI, tying it to an unpredictable Activity lifecycle
  4. for a large folder, partial traversal is still done on UI thread, which means blocking I/O (although it's split into smallers operations, so this might event work for humanely sized folders).

Overall, it looks to me like this design is not going to work for us. The upload driving logicsimply does not belong here (UI) and I'm not surprised that the old author abandoned it. Your refactoring made the code readable to a point where we can actually understand how difficult the situation is, so good work done here. Having said that, I still believe this PR is something we should rethink and get back to the whiteboard.

Before we get too depressed, I'd like to say that the FilesUploader logic is probably salvageable and I see 3 avenues:

Variant 1a - in ui, stateless

Remove the callbacks and make it stateless - construct list of files to upload by recursively traversing the folder, fire-and-forget gigantic request, respecting old design rules. In this variant we probably want to extend FileUpload.Requester with another method that builds fancy Intent with large list of files.

Pros:

  • simple(r)

Cons:

  • probably won't work with big folders as input size is unbounded
  • traversal on UI thread is not feasible due to blocking I/O and ANRs

Variant 1b - in service, stateless

Same as 1a, except that FileUpload (Service) will receive a folder path and perform the traversal to construct the list of files to upload in background. I'm not sure if it does not clash with the funky FileDisplayActivity.onActivityResult(...) -> requestUploadOfFilesFromFileSystem(...) logic, as there is a lot of if-ology there that I don't fully understand, but at first glance it looks like it just handles externally fired Intent

Pros:

  • traversal can be probably done in background easily

Cons:

  • not sure, but it looks like a refactoring rabbit hole
  • still in UI, which is wrong

Variang 2 - in service, stateful

If we can't have a some sort of quasi-functional solution (because of backend round-trips for example, or we need to check things between uploads), FilesUploader-the-wrapper should live inside FileUploader-the-service (with a different name of course), possibly as a properly encoded state machine.

Pros:

  • service is the place where this should live actually

Cons:

  • basically a rewrite
  • service is already suffering from high complexity - this avenue probably induces refactoring rabbit hole around this component

@kilves @tobiasKaminsky @AndyScherzinger IMO this is really tough problem and we should get back to the drawing board.

@ezaquarii
Copy link
Collaborator

ezaquarii commented Nov 28, 2019

@ezaquarii did you delete the long comment chain about javadocs and refactoring?

Sorry, it looks like my review was published before I finished it. This one is final. I moved this comment from attached-to-source-line to a standalone comment, as it mothballed into a summary. :)

@tobiasKaminsky
Copy link
Member

@ezaquarii i totally agree with your analysis, but fear that there is (at least from my side) no time in nearer future to refactor this.

So I would go for 1b as a quick win (then we also do not have Android warning if UI takes too long) and we can also add this enhancement.

Meanwhile we still can & should discuss how to refactor this (in a new issue) with lots of smaller steps, which can be easily done and tested.

@AndyScherzinger
Copy link
Member

I'd also say for now 1b seems to be feasable given the circumstances.

@tobiasKaminsky
Copy link
Member

I switched label back to "1. developing", please change it once it is ready for review

@AndyScherzinger
Copy link
Member

superseeded by #7877

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Upload whole folder
6 participants