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

Introduce BackendService for managing external storage backends #15914

Merged
merged 10 commits into from
Aug 19, 2015

Conversation

RobinMcCorkell
Copy link
Member

Backends are registered to the BackendService through new data structures:

BackendConfig specifies the basic information for a backend: class, human-readable name, visibility to personal/admin, initial priority. It takes an array of BackendParameter's to specifiy the parameters

BackendParameter stores a parameter configuration for an external storage: name of parameter, human-readable name, type of parameter (text, password, hidden, checkbox), flags (optional or not).

When a BackendConfig has checkDependencies() called on it, it will return a (possibly empty) array of BackendDependency's that specify a dependency of the backend. If an empty array is returned it means there are no unhandled dependencies, so the backend can be used.

Storages in the StoragesController now get their parameters validated server-side (fixes a TODO).

TODO:

  • Unit tests
  • Fix the Local storage type appearing in 'allow user mounts' (but cannot be selected)
  • Ask devs if the API is good
  • Integrate hooks (at least login)
  • Convert all existing storages to authentication methods
  • Migrate SMB_OC to SMB + OC auth method
  • Migrate OpenStack to OpenStack + Rackspace separate authentication methods
  • Check custom JS backends still work
  • Check custom JS auth methods work
  • Check multi-auth method backends allow all to be configured

cc @PVince81 @icewind1991 @DeepDiver1975

/**
* External storage backend configuration
*/
class BackendConfig implements \JsonSerializable {
Copy link
Member

Choose a reason for hiding this comment

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

@PVince81 shall we move this into core? In the long run files_external will die and continue to live as isolated apps.
The UI and necessary APIs have to be in core.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, makes sense.

Copy link
Member Author

Choose a reason for hiding this comment

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

@DeepDiver1975 That was one of the main reasons for implementing this - moving BackendService and StorageService over to core is much easier than trying to port OC_Mount_Config over. We just need to make sure we get the API right - that's what the [WIP] tag is for! 😄

@PVince81
Copy link
Contributor

PVince81 commented Jul 3, 2015

CC @jmaciasportela as this will be useful for future work on external storage

@RobinMcCorkell
Copy link
Member Author

This is becoming quite large... if anyone wants to comment on the API, please do so! I'm trying to avoid too many inter-class dependencies, while still providing a clean interface that doesn't involve passing class names everywhere.

Tests are quite comprehensive at this point.

cc @PVince81 @icewind1991 @DeepDiver1975 @MorrisJobke @LukasReschke

*
* @param array $params
*/
public function __construct($params);
Copy link
Member

Choose a reason for hiding this comment

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

array $params

const VALUE_TEXT = 0;
const VALUE_BOOLEAN = 1;
const VALUE_PASSWORD = 2;
const VALUE_HIDDEN = 3;
Copy link
Contributor

Choose a reason for hiding this comment

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

We might want another type VALUE_CUSTOM = 4.
A backend should be then be able to specify a custom type name, and then provide JS code to render and handle it.
This could be done separately, but keep it in mind in case it affects the way how params are rendered. I guess the PHP could only render a placeholder "div" and then trigger an event somehow to trigger the custom JS code to render it.

Copy link
Contributor

Choose a reason for hiding this comment

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

One example could be a dropdown or multiselect. Not sure what other kind of parameters we need, @jmaciasportela do you know ?

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't see how a custom value constant here would be useful - do you mean creating a new placeholder prefix (eww) that is exposed to JS when that value type is set? Or that backends can specify the placeholder prefix they want to use (ewwwwww)? Custom stuff should be done in JS, IMHO.

Copy link
Contributor

Choose a reason for hiding this comment

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

Imagine if there was a time travelling storage where you can specify a date. That one would want to have a date picker, which is currently not possible. So it would specify the custom flag so no field would be rendered, but a value would still be read/written to the config. But maybe that could be achieved with a simple text field, which would be automatically replaced with a date picker by the custom JS code.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ahh, I see, so no rendering of an input at all! Brilliant! /me scurries away to change it

Copy link
Contributor

Choose a reason for hiding this comment

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

Just noticed we have the "HIDDEN" one, maybe that one will already do.
So never mind.

Copy link
Member Author

Choose a reason for hiding this comment

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

Actually on second thoughts, it's not going to work very well. A custom field still needs positioning in the configuration area, so there needs to be some kind of placeholder element that the custom JS can replace, preserving the placement of the inputs. I think the easiest way to accomplish this is via the VALUE_HIDDEN type, since that won't be rendered yet still 'allocates' a space in the configuration area.

Thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

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

Whether space is allocated isn't that bad. With jquery you can just insert elements anywhere.
Most important is that the value is properly read/written in the field.
If using a hidden field, the JS code for that field should make sure to update the value in the hidden field so it can be saved properly. (similar to how we do mount options)

Copy link
Member Author

Choose a reason for hiding this comment

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

Well, it's not necessary to keep the field as a hidden field - the custom JS can replace the element entirely. The advantage of having an element to start with is that the ordering of the fields is guaranteed, and no magic is required to figure out how to insert between the URL and Root fields for example

@PVince81
Copy link
Contributor

We might need a fallback OC_Mount_Config::registerBackend() for older / third party ext storage implementations. I'd consider this method public API even though the naming doesn't match.
The implementation would just be an adapter from the old format to the new one.
Then add @deprecated so we can remove it later.


use \OCA\Files_External\Lib\BackendConfig;
use \OCA\Files_External\Lib\AuthMechConfig;
use \OCA\Files_External\Lib\BackendParameter as Param;
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove as Param ? I didn't find any occurrence

@RobinMcCorkell
Copy link
Member Author

Pluggable authentication mechanisms are here!

capture

The 'Static password' mechanism is just a quick hack I threw together, which always returns the same password (in this case, the correct one for the user). Note that the password never gets stored in mount.json!

Selecting a different authentication mechanism from the dropdown re-creates the relevant configuration fields.

Next up: working with hooks. Some authentication mechanisms might want to listen for hooks (login, namely) and perform some action, such as updating mount.json, as a result. This will be tough without using static methods...

@RobinMcCorkell
Copy link
Member Author

@PVince81 Awww, I hoped to avoid compatibility shims in OC_Mount_Config - eventually I want to kill that class completely.

@PVince81
Copy link
Contributor

We all do 😉
The problem is that without shim we can't merge your PR until all external storage backends have been adapted, even stuff like irods... so better keep compatible for now.

@RobinMcCorkell
Copy link
Member Author

/me sees 'iRODS', runs away.

@RobinMcCorkell
Copy link
Member Author

Actually to be fair, I stole my original implementation idea of SMB_OC from iRODS, so it had some use

@RobinMcCorkell
Copy link
Member Author

15:00:13 PhantomJS 1.9.8 (Linux 0.0.0) ERROR
15:00:13   SyntaxError: Parse error
15:00:13   at /var/jenkins/workspace/pull-request-analyser-ng-simple/label/SLAVE/apps/files_external/js/settings.js:9

Line 9 is the beginning (function() {, and running JSHint + some other checkers locally doesn't flag any errors...

Firefox is happy with the file too

@PVince81
Copy link
Contributor

The blocking ticket #18109 was merged.

This is good to go 👍

@@ -28,8 +28,7 @@
/**
* @var $this \OC\Route\Router
**/
$application = new Application();
$application->registerRoutes(
\OC_Mount_Config::$app->registerRoutes(
Copy link
Contributor

Choose a reason for hiding this comment

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

This causes an error on my side.

{"reqId":"\/ic4rLtLHtT1DuBLflnU","remoteAddr":"127.0.0.1","app":"PHP","message":"Class 'OC_Mount_Config' not found at \/home\/mjob\/Projekte\/owncloud\/master\/apps\/files_external\/appinfo\/routes.php#31","level":3,"time":"2015-08-19T08:05:22+00:00","method":"GET","url":"\/master\/index.php"}

I guess this is caused by the order of loaded files. The class file is hardcoded in the app.php.

Copy link
Contributor

Choose a reason for hiding this comment

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

Somehow this worked just very well for a long time, until I reset my whole instance and enabled the files_external from this branch without having the old one enabled.

Copy link
Contributor

Choose a reason for hiding this comment

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

You might need to merge with master to get the other fix.

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed with rebase

@MorrisJobke
Copy link
Contributor

  • BUG: adding a local mount via personal page I got following error:
{"reqId":"oQb7DRZkUfcunqWk0Sy+","remoteAddr":"127.0.0.1","app":"PHP","message":"Argument 1 passed to OCA\\Files_External\\Lib\\PersonalMount::__construct() must be an instance of OCA\\Files_external\\Service\\UserStoragesService, instance of OC\\Files\\Storage\\Local given, called in \/home\/mjob\/Projekte\/owncloud\/master\/apps\/files_external\/lib\/config\/configadapter.php on line 148 and defined at \/home\/mjob\/Projekte\/owncloud\/master\/apps\/files_external\/lib\/personalmount.php#49","level":0,"time":"2015-08-19T08:13:01+00:00","method":"GET","url":"\/master\/index.php"}
{"reqId":"oQb7DRZkUfcunqWk0Sy+","remoteAddr":"127.0.0.1","app":"PHP","message":"Object of class OC\\Files\\Storage\\StorageFactory could not be converted to string at \/home\/mjob\/Projekte\/owncloud\/master\/lib\/private\/files\/filesystem.php#795","level":0,"time":"2015-08-19T08:13:01+00:00","method":"GET","url":"\/master\/index.php"}
{"reqId":"oQb7DRZkUfcunqWk0Sy+","remoteAddr":"127.0.0.1","app":"PHP","message":"Call to undefined method OCA\\Files_External\\Lib\\PersonalMount::attachStoragesService() at \/home\/mjob\/Projekte\/owncloud\/master\/apps\/files_external\/lib\/config\/configadapter.php#149","level":3,"time":"2015-08-19T08:13:01+00:00","method":"GET","url":"\/master\/index.php"}
$ cat data/mjob/mount.json
{
    "user": {
        "mjob": {
            "\/mjob\/files\/abc": {
                "id": 1,
                "backend": "\\OC\\Files\\Storage\\Local",
                "authMechanism": "builtin::builtin",
                "options": {
                    "datadir": "\/home\/mjob\/tm"
                },
                "storage_id": "3"
            }
        }
    }
}

@PVince81
Copy link
Contributor

@MorrisJobke weird, you shouldn't be able to select "Local" in the personal page.

@MorrisJobke
Copy link
Contributor

@MorrisJobke weird, you shouldn't be able to select "Local" in the personal page.

I also wondered about it, because it was not available some time ago.

@RobinMcCorkell
Copy link
Member Author

@PVince81 @MorrisJobke Unfortunately due to the way backend registration now works, all legacy storages are (by default) available to everyone. I'll patch in a quick fix for that, but it'll be ugly and will touch the legacy registration code a bit.

Robin McCorkell added 10 commits August 19, 2015 10:05
Backends are registered to the BackendService through new data
structures:

Backends are concrete classes, deriving from
\OCA\Files_External\Lib\Backend\Backend. During construction, the
various configuration parameters of the Backend can be set, in a design
similar to Symfony Console.

DefinitionParameter stores a parameter configuration for an external
storage: name of parameter, human-readable name, type of parameter
(text, password, hidden, checkbox), flags (optional or not).

Storages in the StoragesController now get their parameters validated
server-side (fixes a TODO).
UserGlobalStoragesService reads the global storage configuration,
cherry-picking storages applicable to a user. Writing storages through
this service is forbidden, on punishment of throwing an exception.
Storage IDs may also be config hashes when retrieved from this service,
as it is unable to update the storages with real IDs.

As UserGlobalStoragesService and UserStoragesService share a bit of code
relating to users, that has been split into UserTrait. UserTrait also
allows for the user set to be overridden, rather than using the user
from IUserSession.

Config\ConfigAdapter has been reworked to use UserStoragesService and
UserGlobalStoragesService instead of
OC_Mount_Config::getAbsoluteMountPoints(), further reducing dependance
on that horrible static class.
A backend can now specify generic authentication schemes that it
supports, instead of specifying the parameters for its authentication
method directly. This allows multiple authentication mechanisms to be
implemented for a single scheme, providing altered functionality.

This commit introduces the backend framework for this feature, and so at
this point the UI will be broken as the frontend does not specify the
required information.

Terminology:
 - authentication scheme
    Parameter interface for the authentication method. A backend
    supporting the 'password' scheme accepts two parameters, 'user' and
    'password'.
 - authentication mechanism
    Specific mechanism implementing a scheme. Basic mechanisms may
    forward configuration options directly to the backend, more advanced
    ones may lookup parameters or retrieve them from the session

New dropdown selector for external storage configurations to select the
authentication mechanism to be used.

Authentication mechanisms can have visibilities, just like backends.
The API was extended too to make it easier to add/remove visibilities.
In addition, the concept of 'allowed visibility' has been introduced, so
a backend/auth mechanism can force a maximum visibility level (e.g.
Local storage type) that cannot be overridden by configuration in the
web UI.

An authentication mechanism is a fully instantiated implementation. This
allows an implementation to have dependencies injected into it, e.g. an
\OCP\IDB for database operations.

When a StorageConfig is being prepared for mounting, the authentication
mechanism implementation has manipulateStorage() called,
which inserts the relevant authentication method options into the
storage ready for mounting.
Prior to this, the storage class name was stored in mount.json under the
"class" parameter, and the auth mechanism class name under the
"authMechanism" parameter. This decouples the class name from the
identifier used to retrieve the backend or auth mechanism.

Now, backends/auth mechanisms have a unique identifier, which is saved in
the "backend" or "authMechanism" parameter in mount.json respectively.
An identifier is considered unique for the object it references, but the
underlying class may change (e.g. files_external gets pulled into core
and namespaces are modified).
The following functions have been removed:
 - addMountPoint()
 - removeMountPoint()
 - movePersonalMountPoint()

registerBackend() has been rewritten as a shim around BackendService,
allowing legacy code to interact with the new API seamlessly

addMountPoint() was already disconnected from all production code, so
this commit completes the job and removes the function itself, along
with disconnecting and removing related functions. Unit tests have
likewise been removed.

getAbsoluteMountPoints(), getSystemMountPoints() and
getPersonalMountPoints() have been rewritten to use the StoragesServices
The same Application must be used in the settings templates and in
routes, so that any registered backends are correctly seen
Failure to prepare the storage during backend or auth mechanism
manipulation will throw an InsufficientDataForMeaningfulAnswerException,
which is propagated to StorageNotAvailableException in the filesystem
layer via the FailedStorage helper class.

When a storage is unavailable not due to failure, but due to
insufficient data being available, a special 'indeterminate' status is
returned to the configuration UI.
Loading custom JS on a per-backend basis added needless complexity and
made dealing with async required. Now all backends/auth mechanisms load
custom JS in PHP
@scrutinizer-notifier
Copy link

A new inspection was created.

@RobinMcCorkell
Copy link
Member Author

@MorrisJobke That bug you found was caused by me changing PersonalMount in response to @icewind1991 's comment, but then forgetting to change the point that it actually gets used. Fixed now.

@ghost
Copy link

ghost commented Aug 19, 2015

🚀 Test PASSed.🚀
chuck

@RobinMcCorkell
Copy link
Member Author

@PVince81 Could you quickly review that last commit I pushed? Everything else is the same.

@PVince81
Copy link
Contributor

👍

@PVince81
Copy link
Contributor

@MorrisJobke retry?

@MorrisJobke
Copy link
Contributor

Tested and works so far:+1:

MorrisJobke added a commit that referenced this pull request Aug 19, 2015
Introduce BackendService for managing external storage backends
@MorrisJobke MorrisJobke merged commit a9bb6be into master Aug 19, 2015
@MorrisJobke MorrisJobke deleted the ext-backend-register branch August 19, 2015 12:15
@RobinMcCorkell
Copy link
Member Author

plzdontbreak plzdontbreak plzdontbreak 🙈

@lock lock bot locked as resolved and limited conversation to collaborators Aug 10, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants