Merge lp:~michihenning/storage-framework/add-doc into lp:storage-framework/devel
- add-doc
- Merge into devel
Status: | Merged |
---|---|
Approved by: | Michi Henning |
Approved revision: | 132 |
Merged at revision: | 126 |
Proposed branch: | lp:~michihenning/storage-framework/add-doc |
Merge into: | lp:storage-framework/devel |
Diff against target: |
2476 lines (+1687/-122) 25 files modified
CMakeLists.txt (+1/-0) debian/changelog (+1/-0) debian/control (+8/-0) debian/control.in (+8/-0) debian/storage-framework-doc.install (+1/-0) doc/CMakeLists.txt (+20/-0) doc/provider_tut.dox (+588/-0) include/unity/storage/common.h (+39/-18) include/unity/storage/provider/DownloadJob.h (+110/-6) include/unity/storage/provider/Exceptions.h (+157/-17) include/unity/storage/provider/Item.h (+46/-0) include/unity/storage/provider/ProviderBase.h (+225/-39) include/unity/storage/provider/Server.h (+112/-0) include/unity/storage/provider/TempfileUploadJob.h (+43/-5) include/unity/storage/provider/UploadJob.h (+103/-5) include/unity/storage/provider/testing/TestServer.h (+30/-0) include/unity/storage/qt/internal/StorageErrorImpl.h (+1/-0) src/local-provider/LocalDownloadJob.cpp (+2/-2) src/local-provider/LocalProvider.cpp (+11/-1) src/local-provider/LocalUploadJob.cpp (+16/-5) src/qt/internal/ItemImpl.cpp (+1/-1) src/qt/internal/StorageErrorImpl.cpp (+6/-0) src/qt/internal/unmarshal_error.cpp (+1/-1) tests/local-provider/local-provider_test.cpp (+156/-21) tests/remote-client/remote-client_test.cpp (+1/-1) |
To merge this branch: | bzr merge lp:~michihenning/storage-framework/add-doc |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
unity-api-1-bot | continuous-integration | Approve | |
Michi Henning (community) | Approve | ||
James Henstridge | Approve | ||
Review via email: mp+321408@code.launchpad.net |
Commit message
Added tutorial and provider reference documentation.
Description of the change
Added tutorial and provider reference documentation.
I'd appreciate any and all comments. Typos, style/grammar issues, omissions, inaccuracies, etc. In particular, what did I not say that should have been said?
Besides adding doc, this MR also fixes a few other things I stumbled into:
- A few extra tests for corner cases
- Previously protected members in UploadJob, TempfileUploadJOb, and DownloadJob are now private.
- Attempts to delete or move a root now throw PermissionException instead of LogicException.
- Attempts to list a file or download/upload a folder now throw LogicException instead of InvalidArgument
unity-api-1-bot (unity-api-1-bot) wrote : | # |
unity-api-1-bot (unity-api-1-bot) wrote : | # |
PASSED: Continuous integration, rev:128
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
Click here to trigger a rebuild:
https:/
Marcus Tomlinson (marcustomlinson) wrote : | # |
First pass spell checking (inline)
James Henstridge (jamesh) wrote : | # |
Some comments left inline.
unity-api-1-bot (unity-api-1-bot) wrote : | # |
FAILED: Continuous integration, rev:130
https:/
Executed test runs:
FAILURE: https:/
FAILURE: https:/
Click here to trigger a rebuild:
https:/
unity-api-1-bot (unity-api-1-bot) wrote : | # |
PASSED: Continuous integration, rev:131
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
Click here to trigger a rebuild:
https:/
Michi Henning (michihenning) wrote : | # |
Thanks heaps for the reviews, guys!
I've fixed the spelling mistakes (plus a few others I stumbled across).
I've responded to a few comments inline. Where I didn't respond, I believe I've addressed the concern. Let me know if something still doesn't look right please!
unity-api-1-bot (unity-api-1-bot) wrote : | # |
FAILED: Autolanding.
More details in the following jenkins job:
https:/
Executed test runs:
FAILURE: https:/
FAILURE: https:/
- 132. By Michi Henning
-
Merged devel.
Michi Henning (michihenning) wrote : | # |
Re-approving for Jenkins after fixing merge conflict.
unity-api-1-bot (unity-api-1-bot) wrote : | # |
PASSED: Continuous integration, rev:132
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
Click here to trigger a rebuild:
https:/
Preview Diff
1 | === modified file 'CMakeLists.txt' | |||
2 | --- CMakeLists.txt 2017-03-30 00:57:11 +0000 | |||
3 | +++ CMakeLists.txt 2017-04-06 07:42:38 +0000 | |||
4 | @@ -134,6 +134,7 @@ | |||
5 | 134 | add_subdirectory(tests) | 134 | add_subdirectory(tests) |
6 | 135 | add_subdirectory(demo) | 135 | add_subdirectory(demo) |
7 | 136 | add_subdirectory(tools) | 136 | add_subdirectory(tools) |
8 | 137 | add_subdirectory(doc) | ||
9 | 137 | 138 | ||
10 | 138 | enable_coverage_report( | 139 | enable_coverage_report( |
11 | 139 | TARGETS | 140 | TARGETS |
12 | 140 | 141 | ||
13 | === modified file 'debian/changelog' | |||
14 | --- debian/changelog 2017-04-06 04:57:35 +0000 | |||
15 | +++ debian/changelog 2017-04-06 07:42:38 +0000 | |||
16 | @@ -2,6 +2,7 @@ | |||
17 | 2 | 2 | ||
18 | 3 | [ Michi Henning ] | 3 | [ Michi Henning ] |
19 | 4 | * Removed dependency on libgio. | 4 | * Removed dependency on libgio. |
20 | 5 | * Added tutorial and provider reference documentation. | ||
21 | 5 | 6 | ||
22 | 6 | [ James Henstridge ] | 7 | [ James Henstridge ] |
23 | 7 | * Add systemd units for registry and local provider D-Bus services. | 8 | * Add systemd units for registry and local provider D-Bus services. |
24 | 8 | 9 | ||
25 | === modified file 'debian/control' | |||
26 | --- debian/control 2017-03-20 04:51:09 +0000 | |||
27 | +++ debian/control 2017-04-06 07:42:38 +0000 | |||
28 | @@ -110,3 +110,11 @@ | |||
29 | 110 | ${misc:Depends}, | 110 | ${misc:Depends}, |
30 | 111 | Description: Header files for the Storage Framework provider library | 111 | Description: Header files for the Storage Framework provider library |
31 | 112 | Development C++ headers for the provider API. | 112 | Development C++ headers for the provider API. |
32 | 113 | |||
33 | 114 | Package: storage-framework-doc | ||
34 | 115 | Section: doc | ||
35 | 116 | Architecture: all | ||
36 | 117 | Multi-Arch: foreign | ||
37 | 118 | Depends: ${misc:Depends}, | ||
38 | 119 | Description: Documentation for storage-framework-dev | ||
39 | 120 | Tutorial and API reference. | ||
40 | 113 | 121 | ||
41 | === modified file 'debian/control.in' | |||
42 | --- debian/control.in 2017-03-17 05:04:09 +0000 | |||
43 | +++ debian/control.in 2017-04-06 07:42:38 +0000 | |||
44 | @@ -105,3 +105,11 @@ | |||
45 | 105 | ${misc:Depends}, | 105 | ${misc:Depends}, |
46 | 106 | Description: Header files for the Storage Framework provider library | 106 | Description: Header files for the Storage Framework provider library |
47 | 107 | Development C++ headers for the provider API. | 107 | Development C++ headers for the provider API. |
48 | 108 | |||
49 | 109 | Package: storage-framework-doc | ||
50 | 110 | Section: doc | ||
51 | 111 | Architecture: all | ||
52 | 112 | Multi-Arch: foreign | ||
53 | 113 | Depends: ${misc:Depends}, | ||
54 | 114 | Description: Documentation for storage-framework-dev | ||
55 | 115 | Tutorial and API reference. | ||
56 | 108 | 116 | ||
57 | === added file 'debian/storage-framework-doc.install' | |||
58 | --- debian/storage-framework-doc.install 1970-01-01 00:00:00 +0000 | |||
59 | +++ debian/storage-framework-doc.install 2017-04-06 07:42:38 +0000 | |||
60 | @@ -0,0 +1,1 @@ | |||
61 | 1 | usr/share/doc/storage-framework/* | ||
62 | 0 | 2 | ||
63 | === added directory 'doc' | |||
64 | === added file 'doc/CMakeLists.txt' | |||
65 | --- doc/CMakeLists.txt 1970-01-01 00:00:00 +0000 | |||
66 | +++ doc/CMakeLists.txt 2017-04-06 07:42:38 +0000 | |||
67 | @@ -0,0 +1,20 @@ | |||
68 | 1 | find_package(DoxygenBuilder) | ||
69 | 2 | |||
70 | 3 | add_doxygen( | ||
71 | 4 | storage-framework-doc | ||
72 | 5 | INPUT | ||
73 | 6 | ${CMAKE_CURRENT_SOURCE_DIR} | ||
74 | 7 | ${CMAKE_SOURCE_DIR}/include/unity/storage | ||
75 | 8 | STRIP_FROM_PATH | ||
76 | 9 | ${CMAKE_SOURCE_DIR} | ||
77 | 10 | STRIP_FROM_INC_PATH | ||
78 | 11 | ${CMAKE_SOURCE_DIR}/include | ||
79 | 12 | EXCLUDE_PATTERNS | ||
80 | 13 | */internal/* | ||
81 | 14 | */qt/* | ||
82 | 15 | ALL | ||
83 | 16 | ) | ||
84 | 17 | |||
85 | 18 | install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/html | ||
86 | 19 | DESTINATION share/doc/storage-framework | ||
87 | 20 | ) | ||
88 | 0 | 21 | ||
89 | === added file 'doc/provider_tut.dox' | |||
90 | --- doc/provider_tut.dox 1970-01-01 00:00:00 +0000 | |||
91 | +++ doc/provider_tut.dox 2017-04-06 07:42:38 +0000 | |||
92 | @@ -0,0 +1,588 @@ | |||
93 | 1 | /** | ||
94 | 2 | \mainpage Tutorial | ||
95 | 3 | |||
96 | 4 | \section overview Overview | ||
97 | 5 | |||
98 | 6 | The storage framework provides a general-purpose file system abstraction that is independent of any physical storage | ||
99 | 7 | mechanism and the APIs that are needed to manipulate different kinds of storage. This allows client applications to | ||
100 | 8 | access and modify files that exist in remote storage mechanisms (such as | ||
101 | 9 | <a href="https://www.google.com/drive/">Google Drive</a> or <a href="https://owncloud.org/">Owncloud</a>) as well | ||
102 | 10 | as files in the local file system through a common API. | ||
103 | 11 | |||
104 | 12 | The framework includes a provider for local files; to allow access to remote files, the framework | ||
105 | 13 | requires provider implementations that adapt the common API to each cloud service. | ||
106 | 14 | |||
107 | 15 | Each provider implementation runs as a separate DBus service on the session bus; the storage framework | ||
108 | 16 | provides run-time support that takes care of DBus communications, error handling, sanitizing input parameters, | ||
109 | 17 | authorization, and so on. This allows providers to be created without having to handle these details, and enables | ||
110 | 18 | the addition of new providers over time without disturbing existing provider implementations. | ||
111 | 19 | |||
112 | 20 | Ubuntu includes a number of provider implementations, among them an | ||
113 | 21 | <a href="https://code.launchpad.net/storage-provider-webdav">Owncloud provider</a> and an | ||
114 | 22 | <a href="https://launchpad.net/mcloud">mCloud provider</a>. | ||
115 | 23 | |||
116 | 24 | \section semantics File System Semantics | ||
117 | 25 | |||
118 | 26 | The storage framework establishes common semantics that apply to all storage backends. | ||
119 | 27 | Clients must not expect anything not guaranteed by these semantics, and providers must implement the semantics | ||
120 | 28 | as faithfully as possible. | ||
121 | 29 | |||
122 | 30 | The operations that are available are necessarily limited to those that can be supported by the | ||
123 | 31 | majority of backends: | ||
124 | 32 | |||
125 | 33 | - List the root folder (or root folders; some providers support multiple roots) | ||
126 | 34 | |||
127 | 35 | - Create and destroy folders or files | ||
128 | 36 | |||
129 | 37 | - Move or copy a file or folder | ||
130 | 38 | |||
131 | 39 | - List the contents of a folder | ||
132 | 40 | |||
133 | 41 | - Look up a file or folder by name | ||
134 | 42 | |||
135 | 43 | - Look up a file or folder by identity | ||
136 | 44 | |||
137 | 45 | - Upload or download a file | ||
138 | 46 | |||
139 | 47 | - Get additional metadata (such as modification time or mime type) for a file or folder | ||
140 | 48 | |||
141 | 49 | Note that all data transfers can be performed only by uploading or downloading an entire file. The framework | ||
142 | 50 | does not support random access to files. | ||
143 | 51 | |||
144 | 52 | Move and copy operations can be performed only within the same account. If an application wants to copy or move files | ||
145 | 53 | from one account to another, it must download the files from the source account and upload them to the target account | ||
146 | 54 | (and, for a move, delete the source files after uploading them). | ||
147 | 55 | |||
148 | 56 | \subsection accounts Accounts | ||
149 | 57 | |||
150 | 58 | The storage framework is integrated with | ||
151 | 59 | <a href="https://help.ubuntu.com/stable/ubuntu-help/accounts.html">Online Accounts</a>. When a client application | ||
152 | 60 | uses the framework, it first retrieves a list of available accounts from the framework. The application only receives | ||
153 | 61 | those accounts for which the user has authorized access. The application then retrieves the root (or roots) for that | ||
154 | 62 | account, and uses the root to access and modify files and folders underneath that root. | ||
155 | 63 | On the server side, the runtime passes any | ||
156 | 64 | authorization token that is needed to access the cloud backend to the provider implementation; the runtime | ||
157 | 65 | also handles re-authorization in case a token has expired. | ||
158 | 66 | |||
159 | 67 | \subsection identity File and Folder Identity | ||
160 | 68 | |||
161 | 69 | Each file and folder has an identity that is unique within the account that contains the file. | ||
162 | 70 | Identities are opaque strings that are assigned by the provider. (Clients cannot control the identity | ||
163 | 71 | of files and folders). | ||
164 | 72 | |||
165 | 73 | The identity of files and folders is immutable during their life time, and identities | ||
166 | 74 | can be compared for equality and less-than. | ||
167 | 75 | This means that clients can store identities in an external file or a database and expect them to | ||
168 | 76 | continue to refer to the same file or folder even after a process re-start. | ||
169 | 77 | |||
170 | 78 | Identities are not globally unique; different files in different accounts may have the same identity. | ||
171 | 79 | |||
172 | 80 | Identities may not be unique across object life times. For example, if a client creates a file and deletes that | ||
173 | 81 | file again, another file that is created later may get the identity that was used by the earlier file. | ||
174 | 82 | Similarly, moving a file or folder may (or may not) change its identity. | ||
175 | 83 | |||
176 | 84 | \subsection etags ETags | ||
177 | 85 | |||
178 | 86 | Files have an associated <a href="https://en.wikipedia.org/wiki/HTTP_ETag">ETag</a>. The ETag for a file | ||
179 | 87 | changes every time a file is updated. When uploading or downloading a file, a client can specify whether to check | ||
180 | 88 | the ETag of a file. If the client requests the ETag to be checked, the provider performs the | ||
181 | 89 | upload or download only if the ETag sent by the client matches the ETag of the file. This allows clients to ensure | ||
182 | 90 | that a file has not been modified since the client last retrieved the file's metadata. | ||
183 | 91 | |||
184 | 92 | ETags have no particular format; they simply are opaque strings that change each time a file changes. | ||
185 | 93 | |||
186 | 94 | \subsection names File and Folder Names | ||
187 | 95 | |||
188 | 96 | Clients choose the name of files and folders they create. However, providers may modify a name that is provided | ||
189 | 97 | by a client, for example, a provider could map upper-case to lower-case letters. | ||
190 | 98 | The actual name of a new file or folder is returned by the provider as part of the metadata. | ||
191 | 99 | |||
192 | 100 | Different providers place different restrictions on the characters that are allowed as part of a name. | ||
193 | 101 | The following meta-characters are likely to cause problems and should not be used in file or folder names: | ||
194 | 102 | |||
195 | 103 | <tt>/</tt>, <tt>\\</tt>, <tt>*</tt>, <tt>"</tt>, <tt>:</tt>, <tt>\<</tt>, <tt>\></tt>, <tt>|</tt>, <tt>?</tt>, <tt>#</tt>, and <tt>%</tt>. | ||
196 | 104 | |||
197 | 105 | The name of a file or folder is not necessarily unique within its parent folder. (Some cloud services | ||
198 | 106 | allow different files with the same name in the same folder.) Similarly, a single file or folder may | ||
199 | 107 | have more than one name within the same parent folder. (The name of a file or folder does not indicate | ||
200 | 108 | anything about its identity. If you want to establish whether a file is the same as another file, you must | ||
201 | 109 | compare the files' identities, not their names.) | ||
202 | 110 | |||
203 | 111 | The name of a root folder can be anything. (It might be <tt>/</tt>, but could be something else.) | ||
204 | 112 | |||
205 | 113 | \subsection metadata Metadata | ||
206 | 114 | Files and folders have metadata associated with them. | ||
207 | 115 | Metadata items are key–value pairs. The key (a string) identifies the item and determines the type of | ||
208 | 116 | the corresponding value (such as string or integer). | ||
209 | 117 | |||
210 | 118 | Some metadata is guaranteed to always be present (such as file | ||
211 | 119 | size and modification time); other metadata may or may not be available, depending on the provider. (For example, | ||
212 | 120 | not all providers maintain a modification time for directories.) | ||
213 | 121 | |||
214 | 122 | The framework defines a number of well-known metadata keys that are used by the majority of providers (such as | ||
215 | 123 | <tt>size_in_bytes</tt>). In addition, providers can also add provider-specific metadata. Clients can specify | ||
216 | 124 | which metadata items should be returned by the provider. The requested metadata items are hints only: a provider | ||
217 | 125 | may return fewer or more metadata items than were requested by the client. | ||
218 | 126 | |||
219 | 127 | A special metadata key (<tt>__ALL__</tt>) | ||
220 | 128 | indicates that the provider should return all available metadata. If a client does not request any specific | ||
221 | 129 | metadata, the provider returns a default set of items. (Clients can specify which metadata should be returned | ||
222 | 130 | in order to minimize the amount of data that is copied over the network; some providers support extensive metadata | ||
223 | 131 | that consumes considerable bandwidth.) | ||
224 | 132 | |||
225 | 133 | If a provider supplies non-standard metadata items, the item keys have a provider-specific prefix, such as | ||
226 | 134 | <tt>mcloud:</tt> or <tt>gdrive:</tt>. | ||
227 | 135 | |||
228 | 136 | \subsection uploads-downloads Uploads and Downloads | ||
229 | 137 | |||
230 | 138 | Uploads and downloads take place over a UNIX domain socket. When a client requests an upload, the runtime creates | ||
231 | 139 | a socket pair and passes a socket that is open for writing to the client, and a socket that is open for reading | ||
232 | 140 | to the provider. (For downloads, the read and write ends are reversed.) The client and provider write and read | ||
233 | 141 | the data for the file to/from their respective sockets. Once the client has written all the data, it makes one | ||
234 | 142 | final API call to check whether the provider has correctly received all the data. This call returns the new metadata | ||
235 | 143 | for the file (or an error, if the upload failed). | ||
236 | 144 | |||
237 | 145 | Downloads work the same way as uploads, but with the read and write roles reversed. | ||
238 | 146 | |||
239 | 147 | \section provider Implementing a Provider | ||
240 | 148 | |||
241 | 149 | This section provides an overview of the provider API and explains the semantics you are expected to adhere | ||
242 | 150 | to in your provider implementation. We strongly recommend that you also read the source code | ||
243 | 151 | for the <a href="https://code.launchpad.net/storage-provider-webdav">Owncloud provider</a>. It is useful | ||
244 | 152 | to learn how to implement a provider that uses HTTP to access a cloud service. (While the details | ||
245 | 153 | of the interactions with your provider will differ, the principles will be | ||
246 | 154 | very similar.) | ||
247 | 155 | |||
248 | 156 | \subsection provider-overview Overview | ||
249 | 157 | |||
250 | 158 | To implement a provider, you must create an implementation of the abstract | ||
251 | 159 | @ref unity::storage::provider::ProviderBase "ProviderBase" class: | ||
252 | 160 | |||
253 | 161 | \code{.cpp} | ||
254 | 162 | class ProviderBase : public std::enable_shared_from_this<ProviderBase> | ||
255 | 163 | { | ||
256 | 164 | public: | ||
257 | 165 | ProviderBase(); | ||
258 | 166 | virtual ~ProviderBase(); | ||
259 | 167 | |||
260 | 168 | virtual boost::future<ItemList> roots(vector<string> const& keys, | ||
261 | 169 | Context const& context) = 0; | ||
262 | 170 | virtual boost::future<ItemList> lookup(string const& parent_id, | ||
263 | 171 | string const& name, | ||
264 | 172 | vector<string> const& keys, | ||
265 | 173 | Context const& context) = 0; | ||
266 | 174 | // More methods here... | ||
267 | 175 | }; | ||
268 | 176 | \endcode | ||
269 | 177 | |||
270 | 178 | The base class defines a pure virtual method for each operation (such as | ||
271 | 179 | @ref unity::storage::provider::ProviderBase::list "list()", | ||
272 | 180 | @ref unity::storage::provider::ProviderBase::lookup "lookup()", | ||
273 | 181 | @ref unity::storage::provider::ProviderBase::update "update()", etc.) | ||
274 | 182 | |||
275 | 183 | Note that each operation returns a <code>boost::future</code> to the storage framework runtime; | ||
276 | 184 | your operation implementation must make the future ready once the result of the operation is available. | ||
277 | 185 | |||
278 | 186 | For this example, we assume that you want to create a provider for a (hypothetical) <code>MyCloud</code> | ||
279 | 187 | storage service. | ||
280 | 188 | |||
281 | 189 | To connect your implementation class to the runtime, you instantiate a <code>Server</code> instance and call its | ||
282 | 190 | @ref unity::storage::provider::Server::init "init()" and | ||
283 | 191 | @ref unity::storage::provider::Server::run "run()" methods: | ||
284 | 192 | |||
285 | 193 | \code{.cpp} | ||
286 | 194 | class MyCloudProvider : public ProviderBase | ||
287 | 195 | { | ||
288 | 196 | public: | ||
289 | 197 | MyCloudProvider(); | ||
290 | 198 | virtual ~MyCloudProvider(); | ||
291 | 199 | |||
292 | 200 | boost::future<ItemList> roots(vector<string> const& keys, | ||
293 | 201 | Context const& context) override; | ||
294 | 202 | boost::future<ItemList> lookup(string const& parent_id, | ||
295 | 203 | string const& name, | ||
296 | 204 | vector<string> const& keys, | ||
297 | 205 | Context const& context) override; | ||
298 | 206 | // More methods here... | ||
299 | 207 | }; | ||
300 | 208 | |||
301 | 209 | int main(int argc, char* argv[]) | ||
302 | 210 | { | ||
303 | 211 | string const bus_name = "com.acme.StorageFramework.Provider.MyCloud"; | ||
304 | 212 | string const account_service_id = "storage-provider-mycloud"; | ||
305 | 213 | |||
306 | 214 | try | ||
307 | 215 | { | ||
308 | 216 | Server<MyCloudProvider> server(bus_name, account_service_id); | ||
309 | 217 | server.init(argc, argv); | ||
310 | 218 | return server.run(); | ||
311 | 219 | } | ||
312 | 220 | catch (std::exception const& e) | ||
313 | 221 | { | ||
314 | 222 | cerr << argv[0] << ": " << e.what() << endl; | ||
315 | 223 | return 1; | ||
316 | 224 | } | ||
317 | 225 | } | ||
318 | 226 | \endcode | ||
319 | 227 | |||
320 | 228 | The <code>Server</code> class is a template that instantiates your <code>MyCloudProvider</code> class. | ||
321 | 229 | You must provide a DBus name for your service, as well as the service ID with which your provider is | ||
322 | 230 | known to Online Accounts. | ||
323 | 231 | |||
324 | 232 | The call to @ref unity::storage::provider::Server::init "init()" initializes the storage framework runtime, | ||
325 | 233 | connects the service to DBus and, once | ||
326 | 234 | you call @ref unity::storage::provider::Server::run "run()", | ||
327 | 235 | starts an event loop that forwards incoming requests to the methods of your | ||
328 | 236 | <code>MyCloudProvider</code> class. | ||
329 | 237 | |||
330 | 238 | The @ref unity::storage::provider::Server::run "run()" | ||
331 | 239 | method returns after some period of inactivity (30 seconds, by default), so the provider | ||
332 | 240 | process shuts down when it is not needed, and is started automatically if it is not running at the time a client | ||
333 | 241 | request arrives. | ||
334 | 242 | |||
335 | 243 | If you need to pass additional arguments to the <code>MyCloudProvider</code> constructor, you can do so | ||
336 | 244 | by creating a server class of your own that derives from @ref unity::storage::provider::ServerBase "ServerBase". | ||
337 | 245 | That class has an abstract @ref unity::storage::provider::ServerBase::make_provider() "make_provider()" | ||
338 | 246 | factory method that you can override. The runtime calls the factory method to instantiate your provider. | ||
339 | 247 | |||
340 | 248 | \subsection threading Threading Considerations | ||
341 | 249 | |||
342 | 250 | The runtime invokes all methods of your provider implementation on the main thread. Your methods | ||
343 | 251 | <i>must not</i> block for any length of time. Unless you know that a method will return | ||
344 | 252 | immediately (within a few milliseconds), you <i>must</i> implement the method such that it can | ||
345 | 253 | complete in the background (either using asynchronous I/O or a separate thread). | ||
346 | 254 | |||
347 | 255 | The runtime uses <a href="http://doc.qt.io/qt-5/qcoreapplication.html"><code>QCoreApplication</code></a>, | ||
348 | 256 | so your implementation can use anything that uses the Qt or glib event loop. (Alternatively, you can also | ||
349 | 257 | run a separate event loop if you prefer.) | ||
350 | 258 | |||
351 | 259 | When starting an async operation, create a | ||
352 | 260 | <a href="http://www.boost.org/doc/libs/1_63_0/libs/fiber/doc/html/fiber/synchronization/futures/promise.html"><code>boost::promise</code></a> and return <code>promise::get_future()</code>; on completion, | ||
353 | 261 | call <code>set_value()</code> or <code>set_exception()</code> to make the future ready. | ||
354 | 262 | |||
355 | 263 | For a synchronous implementation, you can use | ||
356 | 264 | <a href="http://www.boost.org/doc/libs/1_63_0/doc/html/thread/synchronization.html#thread.synchronization.futures.async"><code>boost::async()</code></a> to spawn a new thread. To indicate errors, call | ||
357 | 265 | <a href="http://www.boost.org/doc/libs/1_63_0/libs/exception/doc/enable_current_exception.html"><code>boost::enable_current_exception()</code></a> | ||
358 | 266 | or <a href="http://www.boost.org/doc/libs/master/libs/exception/doc/throw_exception.html"><code>boost::throw_exception</code></a>. | ||
359 | 267 | |||
360 | 268 | The storage framework runtime is thread-safe, that is, you can up-call into the runtime (for example, to indicate | ||
361 | 269 | an error during an upload) from any thread. The runtime takes care of any necessary locking and moving data | ||
362 | 270 | to the correct thread for marshaling. | ||
363 | 271 | |||
364 | 272 | \subsection provider-error-handling Error Handling | ||
365 | 273 | |||
366 | 274 | All error handling uses exceptions. If you call into the runtime and it detects a problem, the runtime throws | ||
367 | 275 | an exception that derives from <code>std::exception</code> or, for specific well-known problems, from | ||
368 | 276 | @ref unity::storage::provider::StorageException "StorageException". (The code in <code>main()</code> above | ||
369 | 277 | relies on this behavior to report any errors and return non-zero exit status.) | ||
370 | 278 | |||
371 | 279 | If your implementation uses a library that does not transparently handle exceptions (such Qt), you | ||
372 | 280 | must catch exceptions and deal with them as appropriate when you call into the runtime. | ||
373 | 281 | |||
374 | 282 | Conversely, the runtime is exception-safe. If you encounter an error during an operation that | ||
375 | 283 | throws an exception and you let the exception escape, the runtime will intercept the exception, abort the | ||
376 | 284 | operation, and marshal an appropriate error back to the client. (If you throw something unexpected, such as | ||
377 | 285 | <tt>99</tt> or a <code>std::exception</code>, the client receives a generic error.) | ||
378 | 286 | This means that, to indicate an error to the runtime, you can always throw an exception or, alternatively, | ||
379 | 287 | for methods that return a future, return a future that contains the exception. | ||
380 | 288 | |||
381 | 289 | When throwing an exception, be as detailed in the error message as possible and include any details you receive | ||
382 | 290 | from the cloud provider, as well as the identity of the file. Without this, it may be impossible to | ||
383 | 291 | diagnose a problem from log files. | ||
384 | 292 | |||
385 | 293 | The storage framework defines a small number of exceptions that indicate specific error conditions to the client. | ||
386 | 294 | You <i>must</i> use these exception to indicate their respective error conditions, otherwise | ||
387 | 295 | the client only knows that something did not work, but does not know the real cause of the error. <i>All</i> providers | ||
388 | 296 | <i>must</i> adhere to this common set of error semantics, otherwise clients cannot implement meaningful | ||
389 | 297 | error messages and error recovery. | ||
390 | 298 | |||
391 | 299 | All provider exceptions derive from a common abstract base exception class: | ||
392 | 300 | |||
393 | 301 | \code{.cpp} | ||
394 | 302 | class StorageException : public std::exception | ||
395 | 303 | { | ||
396 | 304 | public: | ||
397 | 305 | // ... | ||
398 | 306 | virtual char const* what() const noexcept override; | ||
399 | 307 | |||
400 | 308 | std::string type() const; | ||
401 | 309 | std::string error_message() const; | ||
402 | 310 | }; | ||
403 | 311 | |||
404 | 312 | class RemoteCommsException : public StorageException { ... }; | ||
405 | 313 | class NotExistsException : public StorageException { ... }; | ||
406 | 314 | // etc... | ||
407 | 315 | \endcode | ||
408 | 316 | |||
409 | 317 | The @ref unity::storage::provider::StorageException::type "type()" | ||
410 | 318 | method returns the name of the exception (such as "NotExistsException"), and | ||
411 | 319 | @ref unity::storage::provider::StorageException::error_message "error_message()" | ||
412 | 320 | returns a message you provide when you throw the exception. | ||
413 | 321 | (@ref unity::storage::provider::StorageException::what "what()" | ||
414 | 322 | returns a string that contains both the exception name and the error message.) | ||
415 | 323 | |||
416 | 324 | The concrete derived exceptions indicate error semantics as follows: | ||
417 | 325 | |||
418 | 326 | - @ref unity::storage::provider::RemoteCommsException "RemoteCommsException" | ||
419 | 327 | |||
420 | 328 | This exception indicates an (unexpected) error in the communication between your provider and the remote | ||
421 | 329 | cloud service, such as the remote server being unreachable or returning garbled data. | ||
422 | 330 | |||
423 | 331 | - @ref unity::storage::provider::NotExistsException "NotExistsException" | ||
424 | 332 | |||
425 | 333 | This exception indicates that a file or folder the client wants to access does not exist. You must | ||
426 | 334 | use this exception only if the remote cloud service has <i>authoritatively</i> indicated non-existence | ||
427 | 335 | (such as with an HTTP 404 response). Do not use this exception for non-authoritative errors, such as | ||
428 | 336 | timeouts or similar. | ||
429 | 337 | |||
430 | 338 | - @ref unity::storage::provider::ExistsException "ExistsException" | ||
431 | 339 | |||
432 | 340 | This exception indicates that a file or folder already exists, preventing an operation (such as a create or copy) | ||
433 | 341 | from completing. Use this exception only if the remote cloud service has <i>authoritatively</i> | ||
434 | 342 | indicated this error. Do not use this exception for non-authoritative errors, such as timeouts | ||
435 | 343 | or similar. | ||
436 | 344 | |||
437 | 345 | - @ref unity::storage::provider::ConflictException "ConflictException" | ||
438 | 346 | |||
439 | 347 | This exception indicates an ETag mismatch: the client asked for a file to be updated or downloaded, but | ||
440 | 348 | the file's ETag has changed since. | ||
441 | 349 | |||
442 | 350 | - @ref unity::storage::provider::UnauthorizedException "UnauthorizedException" | ||
443 | 351 | |||
444 | 352 | This exception indicates that the remote cloud service has indicated an authorization failure. Note that | ||
445 | 353 | you must throw this exception only if the cloud service indicates invalid credentials. Do <i>not</i> throw | ||
446 | 354 | this exception for permission errors (such as an attempt to write to a read-only file). In particular, | ||
447 | 355 | do not throw this exception for an HTTP 403 error, which indicates that access to a particular resource | ||
448 | 356 | was denied even though the credentials were valid. | ||
449 | 357 | |||
450 | 358 | Some cloud providers use authorization tokens that expire after some time. If you detect an | ||
451 | 359 | authorization failure in your code and throw this exception, the runtime re-retrieves the token from Online | ||
452 | 360 | Accounts and transparently calls your operation again if the new token differs. If the runtime receives an | ||
453 | 361 | @ref unity::storage::provider::UnauthorizedException "UnauthorizedException" on this second attempt too, | ||
454 | 362 | it returns the error to the client. | ||
455 | 363 | |||
456 | 364 | - @ref unity::storage::provider::PermissionException "PermissionException" | ||
457 | 365 | |||
458 | 366 | This exception indicates that a file or folder cannot be accessed due to insufficient permissions (such as an | ||
459 | 367 | attempt to write to a read-only file). Do <i>not</i> throw this exception for authorization errors. | ||
460 | 368 | |||
461 | 369 | - @ref unity::storage::provider::QuotaException "QuotaException" | ||
462 | 370 | |||
463 | 371 | This exception indicates that the provider has run out of space or that the maximum number of files and/or | ||
464 | 372 | folders was exceeded. | ||
465 | 373 | |||
466 | 374 | - @ref unity::storage::provider::CancelledException "CancelledException" | ||
467 | 375 | |||
468 | 376 | This exception indicates that the client attempted to interact with an upload or download after it was cancelled. | ||
469 | 377 | Due to the way the API is structured, you will not ever need to throw this exception. It is provided for | ||
470 | 378 | completeness and to allow for provider implementations that do not use the storage framework API. | ||
471 | 379 | |||
472 | 380 | - @ref unity::storage::provider::LogicException "LogicException" | ||
473 | 381 | |||
474 | 382 | This exception indicates that the client has used the API incorrectly, such as indicating that an upload has | ||
475 | 383 | completed before it has written all the data. In general, use | ||
476 | 384 | @ref unity::storage::provider::LogicException "LogicException" | ||
477 | 385 | for any errors that relate to operations being called in the wrong order, and for semantic errors | ||
478 | 386 | (such as an attempt to download a folder or to list a file). | ||
479 | 387 | |||
480 | 388 | - @ref unity::storage::provider::InvalidArgumentException "InvalidArgumentException" | ||
481 | 389 | |||
482 | 390 | This exception indicates that the value of an argument supplied by the client is unacceptable, such as | ||
483 | 391 | an empty string where a non-empty string was excepted, or a negative integer where a positive integer was expected. | ||
484 | 392 | |||
485 | 393 | - @ref unity::storage::provider::ResourceException "ResourceException" | ||
486 | 394 | |||
487 | 395 | This is a generic "catch-all" exception that you can throw to indicate unexpected errors that do not fit into | ||
488 | 396 | any of the other categories. For example, <code>ResourceException</code> is appropriate to indicate out of | ||
489 | 397 | file descriptors, failure to locate a configuration file, or any other unexpected system error. You should | ||
490 | 398 | provide as much contextual information about such errors as possible. In particular, unexpected errors | ||
491 | 399 | typically need to be diagnosed from log files. This means that you should provide, at least, the full error | ||
492 | 400 | message you received from the cloud service (or the operating system), together with all other relevant | ||
493 | 401 | details, such as the name and identity of the file, the URL of a failed request, | ||
494 | 402 | any HTTP error information, and so on. | ||
495 | 403 | |||
496 | 404 | - @ref unity::storage::provider::UnknownException "UnknownException" | ||
497 | 405 | |||
498 | 406 | If your code throws an exception that does not derive from <code>StorageException</code>, it returns | ||
499 | 407 | <code>UnknownException</code> to the client. Do not throw this exception explicitly; is is a "catch-all" | ||
500 | 408 | exception that the runtime uses to indicate that something completely unexpected happened. (If a client | ||
501 | 409 | receives this exception from your provider, chances are that you have forgotten to handle an error condition | ||
502 | 410 | somewhere.) | ||
503 | 411 | |||
504 | 412 | \subsection metadata-keys Metadata Keys | ||
505 | 413 | Each method of your provider has a <code>keys</code> parameter of type <code>vector<string></code>. The parameter | ||
506 | 414 | is provided by the client and indicates which metadata items you should include in the metadata you return | ||
507 | 415 | from the operation. | ||
508 | 416 | |||
509 | 417 | If the vector is empty, this indicates that you should return a set of default metadata items. In practice, this | ||
510 | 418 | means that you should return whatever metadata you can cheaply receive from the cloud provider (in terms | ||
511 | 419 | of bandwidth and number of requests). | ||
512 | 420 | |||
513 | 421 | If the vector contains a single entry <code>__ALL__</code>, you should return complete metadata. In particular, | ||
514 | 422 | you should return as many of the well-known metadata items that are defined in | ||
515 | 423 | common.h as possible, plus | ||
516 | 424 | any provider-specific metadata items you may support (prefixed with a provider name, such as <tt>Mycloud:</tt>). | ||
517 | 425 | |||
518 | 426 | If the client provides a specific list of keys, you should restrict the metadata to those items requested by the | ||
519 | 427 | client. This is useful to reduce traffic between your provider and the cloud service, if the cloud service | ||
520 | 428 | requires additional requests for particular metadata items: you need to provide that metadata only if the client asks | ||
521 | 429 | for it. | ||
522 | 430 | |||
523 | 431 | The keys provided by the client are a hint only, that is, it is fine to provide more items than the client asked for | ||
524 | 432 | (if you happen to have them available anyway at no extra cost), and it is legal to not provide all of the items | ||
525 | 433 | the client asked for. | ||
526 | 434 | |||
527 | 435 | If you receive a key that is not one of the keys defined in <code>common.h</code> and is not one of the keys | ||
528 | 436 | you recognize as provider-specific, do <i>not</i> return an error from the operation. Instead, write a log message | ||
529 | 437 | with the details (your provider name, the identity and name of the file and folder, and the invalid key) and | ||
530 | 438 | ignore the invalid key. | ||
531 | 439 | |||
532 | 440 | Note that <code>common.h</code> defines symbolic constants for metadata keys. Use these symbolic constants | ||
533 | 441 | instead of hard-coding the corresponding string literals. | ||
534 | 442 | |||
535 | 443 | \note For files, the <code>SIZE_IN_BYTES</code> and <code>LAST_MODIFIED_TIME</code> metadata items are mandatory; | ||
536 | 444 | you must always supply these, whether the client asks for them or not. | ||
537 | 445 | |||
538 | 446 | \subsection authorization Authorization | ||
539 | 447 | |||
540 | 448 | Each method of your provider has a trailing parameter of type | ||
541 | 449 | @ref unity::storage::provider::Context "Context": | ||
542 | 450 | |||
543 | 451 | \code{.cpp} | ||
544 | 452 | struct Context | ||
545 | 453 | { | ||
546 | 454 | uid_t uid; | ||
547 | 455 | pid_t pid; | ||
548 | 456 | string security_label; | ||
549 | 457 | |||
550 | 458 | Credentials credentials; | ||
551 | 459 | }; | ||
552 | 460 | \endcode | ||
553 | 461 | |||
554 | 462 | The structure contains the user ID, process ID, and the | ||
555 | 463 | <a href="https://help.ubuntu.com/lts/serverguide/apparmor.html">Apparmor</a> security label of the calling client, | ||
556 | 464 | plus credentials that you can use for authorization with the cloud service. In turn, the credentials are a variant | ||
557 | 465 | type that enables authorization with <a href="https://oauth.net/1/">OAuth1</a>, | ||
558 | 466 | <a href="https://oauth.net/2/">OAuth2</a>, as well as with user name and password: | ||
559 | 467 | |||
560 | 468 | \code{.cpp} | ||
561 | 469 | struct NoCredentials | ||
562 | 470 | { | ||
563 | 471 | }; | ||
564 | 472 | |||
565 | 473 | struct OAuth1Credentials | ||
566 | 474 | { | ||
567 | 475 | string consumer_key; | ||
568 | 476 | string consumer_secret; | ||
569 | 477 | string token; | ||
570 | 478 | string token_secret; | ||
571 | 479 | }; | ||
572 | 480 | |||
573 | 481 | struct OAuth2Credentials | ||
574 | 482 | { | ||
575 | 483 | string access_token; | ||
576 | 484 | }; | ||
577 | 485 | |||
578 | 486 | struct PasswordCredentials | ||
579 | 487 | { | ||
580 | 488 | string username; | ||
581 | 489 | string password; | ||
582 | 490 | string host; | ||
583 | 491 | }; | ||
584 | 492 | |||
585 | 493 | typedef boost::variant<boost::blank,OAuth1Credentials,OAuth2Credentials,PasswordCredentials> Credentials; | ||
586 | 494 | \endcode | ||
587 | 495 | |||
588 | 496 | If you receive an authorization error from the cloud provider with the credentials that are passed to you | ||
589 | 497 | by the runtime, you must throw an @ref unity::storage::provider::UnauthorizedException "UnauthorizedException". | ||
590 | 498 | This prompts the runtime to refresh the token if it may have expired and call your your method | ||
591 | 499 | a second time with the new token. If that second attempt also throws | ||
592 | 500 | @ref unity::storage::provider::UnauthorizedException "UnauthorizedException", the runtime propagates the | ||
593 | 501 | error to the client. | ||
594 | 502 | |||
595 | 503 | \subsection download-upload Downloads and Uploads | ||
596 | 504 | |||
597 | 505 | To implement download and upload, you must create implementations of the abstract | ||
598 | 506 | @ref unity::storage::provider::DownloadJob "DownloadJob" and @ref unity::storage::provider::UploadJob "UploadJob" | ||
599 | 507 | classes and return them from your @ref unity::storage::provider::ProviderBase::download "download()" and | ||
600 | 508 | @ref unity::storage::provider::ProviderBase::update "update()" (or | ||
601 | 509 | @ref unity::storage::provider::ProviderBase::create_file "create_file()") methods, respectively. | ||
602 | 510 | The two base classes are very similar, so we focus on @ref unity::storage::provider::DownloadJob "DownloadJob" here. | ||
603 | 511 | |||
604 | 512 | \code{.cpp} | ||
605 | 513 | class MyDownloadJob : public unity::storage::provider::DownloadJob | ||
606 | 514 | { | ||
607 | 515 | public: | ||
608 | 516 | MyDownloadJob(shared_ptr<MyCloudProvider> const& provider, | ||
609 | 517 | string const& item_id, | ||
610 | 518 | string const& match_etag); | ||
611 | 519 | virtual ~LocalDownloadJob(); | ||
612 | 520 | |||
613 | 521 | boost::future<void> cancel() override; | ||
614 | 522 | boost::future<void> finish() override; | ||
615 | 523 | |||
616 | 524 | private: | ||
617 | 525 | // ... | ||
618 | 526 | }; | ||
619 | 527 | \endcode | ||
620 | 528 | |||
621 | 529 | In the implementation of your constructor, you must initiate the download. The download must be able to proceed | ||
622 | 530 | without blocking. How you do this is up to you. You can run the download in a separate thread, use the | ||
623 | 531 | runtime's Qt event loop, or run a separate event loop. (The storage framework runtime does not care.) | ||
624 | 532 | |||
625 | 533 | Your <code>MyDownloadJob</code> receives a socket that is open for writing and connected to the client by calling | ||
626 | 534 | @ref unity::storage::provider::DownloadJob::write_socket "write_socket()" on its base class. You must write the | ||
627 | 535 | data for the file to the socket (from which the client can read it). | ||
628 | 536 | |||
629 | 537 | At some point, the runtime either calls your @ref unity::storage::provider::DownloadJob::cancel "cancel()" method, | ||
630 | 538 | in which case you simply abort the download, or it calls @ref unity::storage::provider::DownloadJob::finish "finish()". | ||
631 | 539 | The purpose of @ref unity::storage::provider::DownloadJob::finish "finish()" is to confirm whether the download | ||
632 | 540 | completed successfully. At this point, you must check whether you have received <a>all</a> of the data for the file | ||
633 | 541 | from the cloud provider and have successfully written it to the socket. If so, you return a ready future; if not, | ||
634 | 542 | you return a future that holds the appropriate @ref unity::storage::provider::StorageException "StorageException" | ||
635 | 543 | to indicate the nature of the error. | ||
636 | 544 | |||
637 | 545 | @ref unity::storage::provider::DownloadJob "DownloadJob" and @ref unity::storage::provider::UploadJob "UploadJob" | ||
638 | 546 | classes also provide a @ref unity::storage::provider::DownloadJob::report_error "report_error()" method that | ||
639 | 547 | you can use to report any errors that arise while your downloader runs in the background. (If you call | ||
640 | 548 | @ref unity::storage::provider::DownloadJob::report_error "report_error()", neither | ||
641 | 549 | @ref unity::storage::provider::DownloadJob::finish "finish()" nor | ||
642 | 550 | @ref unity::storage::provider::DownloadJob::cancel "cancel()" are called by the runtime.) | ||
643 | 551 | |||
644 | 552 | The @ref unity::storage::provider::DownloadJob "DownloadJob" base class also provides a | ||
645 | 553 | @ref unity::storage::provider::DownloadJob::report_complete "report_complete()" method that you call | ||
646 | 554 | to indicate successful completion without having to wait for the call to | ||
647 | 555 | @ref unity::storage::provider::DownloadJob::finish "finish()". | ||
648 | 556 | |||
649 | 557 | \subsection buffering Download and Upload Buffering | ||
650 | 558 | |||
651 | 559 | When implementing your provider, you need to be aware of when (and when not) to buffer data. | ||
652 | 560 | |||
653 | 561 | For downloads, you should write the data to the client socket as soon as you receive it from the cloud provider. | ||
654 | 562 | This is necessary to enable streaming. (A media player should be able to start playing a song or video as soon | ||
655 | 563 | as possible.) | ||
656 | 564 | |||
657 | 565 | For uploads, the situation is the opposite, and you should buffer any uploads that are larger than than a few | ||
658 | 566 | kilobytes in the file system before starting the upload with the provider. | ||
659 | 567 | This is particularly important for mobile devices, where applications may be suspended for extended periods. | ||
660 | 568 | If you write data to the cloud provider while the client is still writing it, and the client application is suspended, | ||
661 | 569 | the upload gets stuck until the client application resumes (by which time your connection with the cloud provider | ||
662 | 570 | may have timed out, or the user may have walked out of reach of the network). | ||
663 | 571 | |||
664 | 572 | Note that, eventually, you will either receive the @ref unity::storage::provider::DownloadJob::finish "finish()" | ||
665 | 573 | call to confirm whether or not there was an error (or a @ref unity::storage::provider::DownloadJob::cancel "cancel()" | ||
666 | 574 | call if the client application dies). This means that, if your cloud provider is able to resume a previously | ||
667 | 575 | interrupted upload, you can re-start an upload that failed due to loss of the network. (Be judicious in how | ||
668 | 576 | aggressively you try to resume though; on a battery-powered device, it is expensive to turn on the radio, so you | ||
669 | 577 | should use a back-off algorithm and give up if the upload cannot be resumed within a reasonable period of time.) | ||
670 | 578 | |||
671 | 579 | \section local-provider The Local Provider | ||
672 | 580 | |||
673 | 581 | The storage framework includes a local provider that stores files in the local file system. | ||
674 | 582 | In a classic environment, the root of the local files is <code>$XDG_DATA_DIR/storage-framework/local</code>; | ||
675 | 583 | in a Snap environment, the root is <code>$SNAP_USER_COMMON/storage-framework/local</code>. | ||
676 | 584 | You can set <code>$SF_LOCAL_PROVIDER_ROOT</code> to change the root directory. (This is intended mainly for testing.) | ||
677 | 585 | |||
678 | 586 | The local provider uses <code>com.canonical.StorageFramework.Provider.Local</code> as the DBus name. The object path | ||
679 | 587 | is <code>/provider/0</code>. | ||
680 | 588 | */ | ||
681 | 0 | 589 | ||
682 | === modified file 'include/unity/storage/common.h' | |||
683 | --- include/unity/storage/common.h 2016-11-03 03:41:24 +0000 | |||
684 | +++ include/unity/storage/common.h 2017-04-06 07:42:38 +0000 | |||
685 | @@ -23,36 +23,57 @@ | |||
686 | 23 | namespace storage | 23 | namespace storage |
687 | 24 | { | 24 | { |
688 | 25 | 25 | ||
689 | 26 | /** | ||
690 | 27 | \brief Indicates the type of an item. | ||
691 | 28 | */ | ||
692 | 29 | |||
693 | 26 | enum class ItemType | 30 | enum class ItemType |
694 | 27 | { | 31 | { |
699 | 28 | file, | 32 | file, /*!< The item represents a file. */ |
700 | 29 | folder, | 33 | folder, /*!< The item represents a (non-root) folder. */ |
701 | 30 | root, | 34 | root, /*!< The item represents a root folder. */ |
702 | 31 | LAST_ENTRY__ | 35 | LAST_ENTRY__ /*!< End of enumeration marker. */ |
703 | 32 | }; | 36 | }; |
704 | 33 | 37 | ||
705 | 38 | /** | ||
706 | 39 | \brief Determines the behavior in case of an ETag mismatch. | ||
707 | 40 | */ | ||
708 | 41 | |||
709 | 34 | enum class ConflictPolicy | 42 | enum class ConflictPolicy |
710 | 35 | { | 43 | { |
713 | 36 | error_if_conflict, | 44 | error_if_conflict, /*!< Return an error if the ETag has changed. */ |
714 | 37 | ignore_conflict, | 45 | ignore_conflict, /*!< Ignore ETag mismatch and overwrite or replace the file. */ |
715 | 38 | overwrite = ignore_conflict, // TODO: remove this, it's here only for compatibility with v1 API | 46 | overwrite = ignore_conflict, // TODO: remove this, it's here only for compatibility with v1 API |
716 | 39 | }; | 47 | }; |
717 | 40 | 48 | ||
718 | 49 | /** | ||
719 | 50 | \brief This namespace defines well-known metadata keys. | ||
720 | 51 | |||
721 | 52 | See \ref common.h for details. | ||
722 | 53 | */ | ||
723 | 54 | |||
724 | 41 | namespace metadata | 55 | namespace metadata |
725 | 42 | { | 56 | { |
726 | 43 | 57 | ||
739 | 44 | static char constexpr SIZE_IN_BYTES[] = "size_in_bytes"; // int64_t, >= 0 | 58 | // Symbolic constants for well-known metadata keys. |
740 | 45 | static char constexpr CREATION_TIME[] = "creation_time"; // String, ISO 8601 format | 59 | |
741 | 46 | static char constexpr LAST_MODIFIED_TIME[] = "last_modified_time"; // String, ISO 8601 format | 60 | // Doxygen bug makes it impossible to document each constant. |
742 | 47 | static char constexpr CHILD_COUNT[] = "child_count"; // int64_t, >= 0 | 61 | |
743 | 48 | static char constexpr DESCRIPTION[] = "description"; // String | 62 | static char constexpr SIZE_IN_BYTES[] = "size_in_bytes"; // int64_t >= 0 |
744 | 49 | static char constexpr DISPLAY_NAME[] = "display_name"; // String | 63 | static char constexpr CREATION_TIME[] = "creation_time"; // string, ISO 8601 format |
745 | 50 | static char constexpr FREE_SPACE_BYTES[] = "free_space_bytes"; // int64_t, >= 0 | 64 | static char constexpr LAST_MODIFIED_TIME[] = "last_modified_time"; // string, ISO 8601 format |
746 | 51 | static char constexpr USED_SPACE_BYTES[] = "used_space_bytes"; // int64_t, >= 0 | 65 | static char constexpr CHILD_COUNT[] = "child_count"; // int64_t >= 0 |
747 | 52 | static char constexpr CONTENT_TYPE[] = "content_type"; // String | 66 | static char constexpr DESCRIPTION[] = "description"; // string |
748 | 53 | static char constexpr WRITABLE[] = "writable"; // Bool | 67 | static char constexpr DISPLAY_NAME[] = "display_name"; // string |
749 | 54 | static char constexpr MD5[] = "md5"; // String | 68 | static char constexpr FREE_SPACE_BYTES[] = "free_space_bytes"; // int64_t >= 0 |
750 | 55 | static char constexpr DOWNLOAD_URL[] = "download_url"; // String | 69 | static char constexpr USED_SPACE_BYTES[] = "used_space_bytes"; // int64_t >= 0 |
751 | 70 | static char constexpr CONTENT_TYPE[] = "content_type"; // string | ||
752 | 71 | static char constexpr WRITABLE[] = "writable"; // int64_t, 0 or 1 | ||
753 | 72 | static char constexpr MD5[] = "md5"; // string | ||
754 | 73 | static char constexpr DOWNLOAD_URL[] = "download_url"; // string | ||
755 | 74 | |||
756 | 75 | // A single-element vector containing the key ALL indicates that the client would like all available | ||
757 | 76 | // metadata to be returned by the provider. | ||
758 | 56 | 77 | ||
759 | 57 | static char constexpr ALL[] = "__ALL__"; | 78 | static char constexpr ALL[] = "__ALL__"; |
760 | 58 | 79 | ||
761 | 59 | 80 | ||
762 | === modified file 'include/unity/storage/provider/DownloadJob.h' | |||
763 | --- include/unity/storage/provider/DownloadJob.h 2016-07-12 02:22:05 +0000 | |||
764 | +++ include/unity/storage/provider/DownloadJob.h 2017-04-06 07:42:38 +0000 | |||
765 | @@ -37,25 +37,129 @@ | |||
766 | 37 | class ProviderInterface; | 37 | class ProviderInterface; |
767 | 38 | } | 38 | } |
768 | 39 | 39 | ||
769 | 40 | /** | ||
770 | 41 | \brief Abstract base class for download implementations. | ||
771 | 42 | |||
772 | 43 | When the runtime calls ProviderBase::download(), you must return | ||
773 | 44 | a new downloader instance that derives from DownloadJob. Your implementation is responsible | ||
774 | 45 | for retrieving the file's data from the cloud provider and writing it to | ||
775 | 46 | the socket provided by the runtime. | ||
776 | 47 | |||
777 | 48 | You can implement your downloader | ||
778 | 49 | any way you wish, such as by running the download in a separate thread, or | ||
779 | 50 | by using async I/O driven by the runtime's (or any other) event loop. | ||
780 | 51 | |||
781 | 52 | The runtime invokes all methods on the downloader from the main thread. | ||
782 | 53 | */ | ||
783 | 54 | |||
784 | 40 | class UNITY_STORAGE_EXPORT DownloadJob | 55 | class UNITY_STORAGE_EXPORT DownloadJob |
785 | 41 | { | 56 | { |
786 | 42 | public: | 57 | public: |
787 | 58 | /** | ||
788 | 59 | \brief Construct a downloader. | ||
789 | 60 | \param download_id An identifier for this particular download. You can use any non-empty string, | ||
790 | 61 | as long as it is unique among all downloads that are in progress within the corresponding account. | ||
791 | 62 | (A simple incrementing counter will work fine, or you can use the download identifier you receive | ||
792 | 63 | from the cloud service.) | ||
793 | 64 | The runtime uses the <code>download_id</code> to distinguish different downloads that may be | ||
794 | 65 | in progress concurrently, and it ensures that each ID can be used only by its corresponding client. | ||
795 | 66 | */ | ||
796 | 43 | DownloadJob(std::string const& download_id); | 67 | DownloadJob(std::string const& download_id); |
797 | 44 | virtual ~DownloadJob(); | 68 | virtual ~DownloadJob(); |
798 | 45 | 69 | ||
799 | 70 | /** | ||
800 | 71 | \brief Returns the download ID. | ||
801 | 72 | \return The value of the <code>download_id</code> parameter that was passed to the constructor. | ||
802 | 73 | */ | ||
803 | 46 | std::string const& download_id() const; | 74 | std::string const& download_id() const; |
804 | 75 | |||
805 | 76 | /** | ||
806 | 77 | \brief Returns the socket to write the file contents to. | ||
807 | 78 | \return A socket that is open for writing. You must write the file contents to this socket (which is | ||
808 | 79 | connected to the client by the runtime). | ||
809 | 80 | */ | ||
810 | 47 | int write_socket() const; | 81 | int write_socket() const; |
811 | 48 | 82 | ||
815 | 49 | // If the result of the download is reported with either of the | 83 | /** |
816 | 50 | // following two functions, then neither cancel() or finish() will | 84 | \brief Informs the runtime that a download completed successfully. |
817 | 51 | // be called. | 85 | |
818 | 86 | This method makes it unnecessary to wait for the runtime to call finish() in order to confirm | ||
819 | 87 | whether a download completed successfully. You can call report_complete() as soon as you have | ||
820 | 88 | written all of a file's data to the download socket and closed it successfully. This can be convenient | ||
821 | 89 | if a download runs in a separate thread or event loop because there is no need to store success/error state | ||
822 | 90 | for use in finish(). | ||
823 | 91 | |||
824 | 92 | You can call report_complete() from an arbitrary thread. | ||
825 | 93 | |||
826 | 94 | If you call report_complete(), the runtime guarantees that neither finish() nor cancel() will be called, so | ||
827 | 95 | you must reclaim any resources associated with the download before calling report_complete(). | ||
828 | 96 | \see report_error(), finish() | ||
829 | 97 | */ | ||
830 | 52 | void report_complete(); | 98 | void report_complete(); |
833 | 53 | void report_error(std::exception_ptr p); | 99 | |
834 | 54 | 100 | /** | |
835 | 101 | \brief Informs the runtime that a download encountered an error. | ||
836 | 102 | |||
837 | 103 | As for report_complete(), you can call report_error() as soon as you encounter an error during a download | ||
838 | 104 | (such as losing the connection to the cloud provider, or getting an error when writing to the download | ||
839 | 105 | socket). | ||
840 | 106 | |||
841 | 107 | You can call report_error() from an arbitrary thread. | ||
842 | 108 | |||
843 | 109 | If you call report_error(), the runtime guarantees that neither finish() nor cancel() will be called, so | ||
844 | 110 | you must reclaim any resources associated with the download before calling report_error(). | ||
845 | 111 | |||
846 | 112 | \param storage_exception You <i>must</i> pass a StorageException to indicate the reason for the failure. | ||
847 | 113 | \see report_complete(), finish() | ||
848 | 114 | */ | ||
849 | 115 | void report_error(std::exception_ptr storage_exception); | ||
850 | 116 | |||
851 | 117 | /** | ||
852 | 118 | \brief Cancel this download. | ||
853 | 119 | |||
854 | 120 | The runtime calls this method when a client explicitly cancels a download or crashes. | ||
855 | 121 | Your implementation should reclaim all resources that are used by the download. In particular, | ||
856 | 122 | you should stop writing any more data and close the download socket. In addition, | ||
857 | 123 | you should reclaim any resources (such as open connections) that are associated | ||
858 | 124 | with the download from the cloud provider (possibly after informing the cloud provider of | ||
859 | 125 | the cancellation). | ||
860 | 126 | |||
861 | 127 | The runtime guarantees that cancel() will be called only once, and that finish() will not be called | ||
862 | 128 | if cancel() was called. | ||
863 | 129 | |||
864 | 130 | If any errors are encountered, you <i>must</i> report them by returning a future that stores | ||
865 | 131 | a StorageException. Do <i>not</i> call report_error() from inside cancel(). | ||
866 | 132 | |||
867 | 133 | \return A future that becomes ready (or contains a StorageException) once cancellation is complete. | ||
868 | 134 | */ | ||
869 | 55 | virtual boost::future<void> cancel() = 0; | 135 | virtual boost::future<void> cancel() = 0; |
870 | 136 | |||
871 | 137 | /** | ||
872 | 138 | \brief Finalize this download. | ||
873 | 139 | |||
874 | 140 | The runtime calls this method when a client finishes a download. Your implementation <i>must</i> verify | ||
875 | 141 | that it has successfully written <i>all</i> of the file's data at this point. | ||
876 | 142 | If not, it <i>must</i> store a LogicException | ||
877 | 143 | in the returned future. (This is essential to make sure that the client can distinguish orderly socket | ||
878 | 144 | closure from disorderly closure and has not received partial data for the file.) | ||
879 | 145 | |||
880 | 146 | You should close the download socket and reclaim any resources (such as open | ||
881 | 147 | connections) that are associated with the download from the cloud provider. | ||
882 | 148 | |||
883 | 149 | The runtime guarantees that finish() will be called only once, and that cancel() will not be called | ||
884 | 150 | if finish() was called. | ||
885 | 151 | |||
886 | 152 | If any errors are encountered, you <i>must</i> report them by returning a future that stores | ||
887 | 153 | a StorageException. Do <i>not</i> call report_error() from inside finish(). | ||
888 | 154 | |||
889 | 155 | Do <i>not</i> call report_complete() from inside finish(). | ||
890 | 156 | |||
891 | 157 | \return A future that becomes ready (or contains a StorageException) once the download is complete. | ||
892 | 158 | \see report_complete(), report_error() | ||
893 | 159 | */ | ||
894 | 56 | virtual boost::future<void> finish() = 0; | 160 | virtual boost::future<void> finish() = 0; |
895 | 57 | 161 | ||
897 | 58 | protected: | 162 | private: |
898 | 59 | DownloadJob(internal::DownloadJobImpl *p) UNITY_STORAGE_HIDDEN; | 163 | DownloadJob(internal::DownloadJobImpl *p) UNITY_STORAGE_HIDDEN; |
899 | 60 | internal::DownloadJobImpl *p_ = nullptr; | 164 | internal::DownloadJobImpl *p_ = nullptr; |
900 | 61 | 165 | ||
901 | 62 | 166 | ||
902 | === modified file 'include/unity/storage/provider/Exceptions.h' | |||
903 | --- include/unity/storage/provider/Exceptions.h 2017-03-30 02:52:53 +0000 | |||
904 | +++ include/unity/storage/provider/Exceptions.h 2017-04-06 07:42:38 +0000 | |||
905 | @@ -37,20 +37,39 @@ | |||
906 | 37 | // - src/qt/internal/unmarshal_error.cpp | 37 | // - src/qt/internal/unmarshal_error.cpp |
907 | 38 | 38 | ||
908 | 39 | /** | 39 | /** |
910 | 40 | \brief Base exception class for all server-side exceptions. | 40 | \brief Abstract base exception class for all server-side exceptions. |
911 | 41 | */ | 41 | */ |
912 | 42 | |||
913 | 42 | class UNITY_STORAGE_EXPORT StorageException : public std::exception | 43 | class UNITY_STORAGE_EXPORT StorageException : public std::exception |
914 | 43 | { | 44 | { |
915 | 44 | public: | 45 | public: |
916 | 46 | /** | ||
917 | 47 | \brief Constructs a StorageException. | ||
918 | 48 | \param exception_type The concrete type name of the exception, such as "ConflictException". | ||
919 | 49 | \param error_message The error message text for the exception. | ||
920 | 50 | */ | ||
921 | 45 | StorageException(std::string const& exception_type, std::string const& error_message); | 51 | StorageException(std::string const& exception_type, std::string const& error_message); |
922 | 46 | 52 | ||
923 | 47 | protected: | 53 | protected: |
924 | 48 | ~StorageException(); | 54 | ~StorageException(); |
925 | 49 | 55 | ||
926 | 50 | public: | 56 | public: |
927 | 57 | /** | ||
928 | 58 | \brief Returns an error string. | ||
929 | 59 | \return The error message prefixed by the exception type, such as "ConflictException: ETag mismatch". | ||
930 | 60 | */ | ||
931 | 51 | virtual char const* what() const noexcept override; | 61 | virtual char const* what() const noexcept override; |
932 | 52 | 62 | ||
933 | 63 | /** | ||
934 | 64 | \brief Returns the exception type name. | ||
935 | 65 | \return The value of the <code>exception_type</code> parameter that was passed to the constructor. | ||
936 | 66 | */ | ||
937 | 53 | std::string type() const; | 67 | std::string type() const; |
938 | 68 | |||
939 | 69 | /** | ||
940 | 70 | \brief Returns the except error message. | ||
941 | 71 | \return The value of the <code>error_message</code> parameter that was passed to the constructor. | ||
942 | 72 | */ | ||
943 | 54 | std::string error_message() const; | 73 | std::string error_message() const; |
944 | 55 | 74 | ||
945 | 56 | private: | 75 | private: |
946 | @@ -61,23 +80,45 @@ | |||
947 | 61 | 80 | ||
948 | 62 | /** | 81 | /** |
949 | 63 | \brief Indicates errors in the communication between the storage provider and the cloud service. | 82 | \brief Indicates errors in the communication between the storage provider and the cloud service. |
950 | 83 | |||
951 | 84 | Use this exception for unexpected communication errors, such as the remote server being unreachable | ||
952 | 85 | or returning garbled data. | ||
953 | 64 | */ | 86 | */ |
954 | 87 | |||
955 | 65 | class UNITY_STORAGE_EXPORT RemoteCommsException : public StorageException | 88 | class UNITY_STORAGE_EXPORT RemoteCommsException : public StorageException |
956 | 66 | { | 89 | { |
957 | 67 | public: | 90 | public: |
958 | 91 | /** | ||
959 | 92 | \brief Construct a RemoteCommsException. | ||
960 | 93 | \param error_message The error message for the exception. | ||
961 | 94 | */ | ||
962 | 68 | RemoteCommsException(std::string const& error_message); | 95 | RemoteCommsException(std::string const& error_message); |
963 | 69 | ~RemoteCommsException(); | 96 | ~RemoteCommsException(); |
964 | 70 | }; | 97 | }; |
965 | 71 | 98 | ||
966 | 72 | /** | 99 | /** |
968 | 73 | \brief Indicates that an item does not exist or could not be found. | 100 | \brief Indicates that an item does not exist. |
969 | 101 | |||
970 | 102 | Use this exception only if the remote cloud service has <i>authoritatively</i> indicated non-existence | ||
971 | 103 | (such as with an HTTP 404 response). Do not use this exception for non-authoritative errors, such as | ||
972 | 104 | timeouts or similar. | ||
973 | 74 | */ | 105 | */ |
974 | 106 | |||
975 | 75 | class UNITY_STORAGE_EXPORT NotExistsException : public StorageException | 107 | class UNITY_STORAGE_EXPORT NotExistsException : public StorageException |
976 | 76 | { | 108 | { |
977 | 77 | public: | 109 | public: |
978 | 110 | /** | ||
979 | 111 | \brief Construct a RemoteCommsException. | ||
980 | 112 | \param error_message The error message for the exception. | ||
981 | 113 | \param key The name or identity of the non-existent item. | ||
982 | 114 | */ | ||
983 | 78 | NotExistsException(std::string const& error_message, std::string const& key); | 115 | NotExistsException(std::string const& error_message, std::string const& key); |
984 | 79 | ~NotExistsException(); | 116 | ~NotExistsException(); |
985 | 80 | 117 | ||
986 | 118 | /** | ||
987 | 119 | \brief Return the key. | ||
988 | 120 | \return The value of the <code>key</code> parameter that was passed to the constructor. | ||
989 | 121 | */ | ||
990 | 81 | std::string key() const; | 122 | std::string key() const; |
991 | 82 | 123 | ||
992 | 83 | private: | 124 | private: |
993 | @@ -85,15 +126,35 @@ | |||
994 | 85 | }; | 126 | }; |
995 | 86 | 127 | ||
996 | 87 | /** | 128 | /** |
998 | 88 | \brief Indicates that an item cannot be created because it exists already. | 129 | \brief Indicates that an item already exists. |
999 | 130 | |||
1000 | 131 | Use this exception only if the remote cloud service has <i>authoritatively</i> indicated that an operation | ||
1001 | 132 | cannot be carried out because an item exists already. Do not use this exception for non-authoritative errors, | ||
1002 | 133 | such as timeouts or similar. | ||
1003 | 89 | */ | 134 | */ |
1004 | 135 | |||
1005 | 90 | class UNITY_STORAGE_EXPORT ExistsException : public StorageException | 136 | class UNITY_STORAGE_EXPORT ExistsException : public StorageException |
1006 | 91 | { | 137 | { |
1007 | 92 | public: | 138 | public: |
1008 | 139 | /** | ||
1009 | 140 | \brief Construct an ExistsException. | ||
1010 | 141 | \param error_message The error message for the exception. | ||
1011 | 142 | \param identity The identity of the item. | ||
1012 | 143 | \param name The name of the item. | ||
1013 | 144 | */ | ||
1014 | 93 | ExistsException(std::string const& error_message, std::string const& identity, std::string const& name); | 145 | ExistsException(std::string const& error_message, std::string const& identity, std::string const& name); |
1015 | 94 | ~ExistsException(); | 146 | ~ExistsException(); |
1016 | 95 | 147 | ||
1017 | 148 | /** | ||
1018 | 149 | \brief Return the identity. | ||
1019 | 150 | \return The value of the <code>identity</code> parameter that was passed to the constructor. | ||
1020 | 151 | */ | ||
1021 | 96 | std::string native_identity() const; | 152 | std::string native_identity() const; |
1022 | 153 | |||
1023 | 154 | /** | ||
1024 | 155 | \brief Return the name of the item. | ||
1025 | 156 | \return The value of the <code>name</code> parameter that was passed to the constructor. | ||
1026 | 157 | */ | ||
1027 | 97 | std::string name() const; | 158 | std::string name() const; |
1028 | 98 | 159 | ||
1029 | 99 | private: | 160 | private: |
1030 | @@ -102,33 +163,42 @@ | |||
1031 | 102 | }; | 163 | }; |
1032 | 103 | 164 | ||
1033 | 104 | /** | 165 | /** |
1035 | 105 | \brief Indicates that an upload or download detected a version mismatch. | 166 | \brief Indicates that an upload or download detected a ETag mismatch. |
1036 | 106 | */ | 167 | */ |
1037 | 168 | |||
1038 | 107 | class UNITY_STORAGE_EXPORT ConflictException : public StorageException | 169 | class UNITY_STORAGE_EXPORT ConflictException : public StorageException |
1039 | 108 | { | 170 | { |
1040 | 109 | public: | 171 | public: |
1041 | 172 | /** | ||
1042 | 173 | \brief Construct a ConflictException. | ||
1043 | 174 | \param error_message The error message for the exception. | ||
1044 | 175 | */ | ||
1045 | 110 | ConflictException(std::string const& error_message); | 176 | ConflictException(std::string const& error_message); |
1046 | 111 | ~ConflictException(); | 177 | ~ConflictException(); |
1047 | 112 | }; | 178 | }; |
1048 | 113 | 179 | ||
1049 | 114 | /** | 180 | /** |
1051 | 115 | \brief Indicates that an operation failed because the authentication credentials are invalid or expired. | 181 | \brief Indicates that an operation failed because the credentials are invalid or have expired. |
1052 | 116 | 182 | ||
1053 | 117 | A provider implementation must throw this exception if it cannot reach | 183 | A provider implementation must throw this exception if it cannot reach |
1055 | 118 | its provider because the credentials are invalid. Do not throw this | 184 | its provider because the credentials are invalid. Do <i>not</i> throw this |
1056 | 119 | exception if the credentials are valid, but an operation failed due to | 185 | exception if the credentials are valid, but an operation failed due to |
1057 | 120 | insufficient permission for an item (such as an attempt to write to a | 186 | insufficient permission for an item (such as an attempt to write to a |
1058 | 121 | read-only file). | 187 | read-only file). |
1059 | 122 | 188 | ||
1063 | 123 | Typically, this will cause the request to be retried after refreshing | 189 | Typically, this exception will cause the runtime to retry the operation after refreshing |
1064 | 124 | the authentication credentials, but may be returned to the client on | 190 | the credentials; the exception may be returned to the client on repeated failures. |
1062 | 125 | repeated failures. | ||
1065 | 126 | 191 | ||
1066 | 127 | \see PermissionException | 192 | \see PermissionException |
1067 | 128 | */ | 193 | */ |
1068 | 194 | |||
1069 | 129 | class UNITY_STORAGE_EXPORT UnauthorizedException : public StorageException | 195 | class UNITY_STORAGE_EXPORT UnauthorizedException : public StorageException |
1070 | 130 | { | 196 | { |
1071 | 131 | public: | 197 | public: |
1072 | 198 | /** | ||
1073 | 199 | \brief Construct an UnauthorizedException. | ||
1074 | 200 | \param error_message The error message for the exception. | ||
1075 | 201 | */ | ||
1076 | 132 | UnauthorizedException(std::string const& error_message); | 202 | UnauthorizedException(std::string const& error_message); |
1077 | 133 | ~UnauthorizedException(); | 203 | ~UnauthorizedException(); |
1078 | 134 | }; | 204 | }; |
1079 | @@ -139,69 +209,126 @@ | |||
1080 | 139 | A provider implementation must throw this exception if it can | 209 | A provider implementation must throw this exception if it can |
1081 | 140 | authenticate with its provider, but the provider denied the operation | 210 | authenticate with its provider, but the provider denied the operation |
1082 | 141 | due to insufficient permission for an item (such as an attempt to | 211 | due to insufficient permission for an item (such as an attempt to |
1084 | 142 | write to a read-only file). Do not throw this exception for failure to | 212 | write to a read-only file). Do <i>not</i> throw this exception for failure to |
1085 | 143 | authenticate with the provider. | 213 | authenticate with the provider. |
1086 | 144 | 214 | ||
1087 | 145 | \see UnauthorizedException | 215 | \see UnauthorizedException |
1088 | 146 | */ | 216 | */ |
1089 | 217 | |||
1090 | 147 | class UNITY_STORAGE_EXPORT PermissionException : public StorageException | 218 | class UNITY_STORAGE_EXPORT PermissionException : public StorageException |
1091 | 148 | { | 219 | { |
1092 | 149 | public: | 220 | public: |
1093 | 221 | /** | ||
1094 | 222 | \brief Construct a PermissionException. | ||
1095 | 223 | \param error_message The error message for the exception. | ||
1096 | 224 | */ | ||
1097 | 150 | PermissionException(std::string const& error_message); | 225 | PermissionException(std::string const& error_message); |
1098 | 151 | ~PermissionException(); | 226 | ~PermissionException(); |
1099 | 152 | }; | 227 | }; |
1100 | 153 | 228 | ||
1101 | 154 | /** | 229 | /** |
1103 | 155 | \brief Indicates that an update failed because the provider ran out of space. | 230 | \brief Indicates that an update failed because the provider ran out of space or exceeded |
1104 | 231 | the maximum number of files or folders. | ||
1105 | 156 | */ | 232 | */ |
1106 | 233 | |||
1107 | 157 | class UNITY_STORAGE_EXPORT QuotaException : public StorageException | 234 | class UNITY_STORAGE_EXPORT QuotaException : public StorageException |
1108 | 158 | { | 235 | { |
1109 | 159 | public: | 236 | public: |
1110 | 237 | /** | ||
1111 | 238 | \brief Construct a QuotaException. | ||
1112 | 239 | \param error_message The error message for the exception. | ||
1113 | 240 | */ | ||
1114 | 160 | QuotaException(std::string const& error_message); | 241 | QuotaException(std::string const& error_message); |
1115 | 161 | ~QuotaException(); | 242 | ~QuotaException(); |
1116 | 162 | }; | 243 | }; |
1117 | 163 | 244 | ||
1118 | 164 | /** | 245 | /** |
1119 | 165 | \brief Indicates that an upload or download was cancelled before it could complete. | 246 | \brief Indicates that an upload or download was cancelled before it could complete. |
1120 | 247 | \note Due to the way the provider API is structured, you will not ever need to throw this exception. It is provided for | ||
1121 | 248 | completeness and to allow for provider implementations that do not use the storage framework API. | ||
1122 | 166 | */ | 249 | */ |
1123 | 250 | |||
1124 | 167 | class UNITY_STORAGE_EXPORT CancelledException : public StorageException | 251 | class UNITY_STORAGE_EXPORT CancelledException : public StorageException |
1125 | 168 | { | 252 | { |
1126 | 169 | public: | 253 | public: |
1127 | 254 | /** | ||
1128 | 255 | \brief Construct a CancelledException. | ||
1129 | 256 | \param error_message The error message for the exception. | ||
1130 | 257 | */ | ||
1131 | 170 | CancelledException(std::string const& error_message); | 258 | CancelledException(std::string const& error_message); |
1132 | 171 | ~CancelledException(); | 259 | ~CancelledException(); |
1133 | 172 | }; | 260 | }; |
1134 | 173 | 261 | ||
1135 | 174 | /** | 262 | /** |
1137 | 175 | \brief Indicates incorrect use of the API, such as calling methods in the wrong order. | 263 | \brief Indicates incorrect use of the API. |
1138 | 264 | |||
1139 | 265 | Use this exception for errors that the client could avoid, such as calling methods in the wrong order or attempting | ||
1140 | 266 | to list the contents of file (as opposed to a folder). | ||
1141 | 267 | \note Do <i>not</i> throw this exception to indicate arguments that are malformed or out of range. | ||
1142 | 268 | \see InvalidArgumentException | ||
1143 | 176 | */ | 269 | */ |
1144 | 177 | class UNITY_STORAGE_EXPORT LogicException : public StorageException | 270 | class UNITY_STORAGE_EXPORT LogicException : public StorageException |
1145 | 178 | { | 271 | { |
1146 | 179 | public: | 272 | public: |
1147 | 273 | /** | ||
1148 | 274 | \brief Construct a LogicException. | ||
1149 | 275 | \param error_message The error message for the exception. | ||
1150 | 276 | */ | ||
1151 | 180 | LogicException(std::string const& error_message); | 277 | LogicException(std::string const& error_message); |
1152 | 181 | ~LogicException(); | 278 | ~LogicException(); |
1153 | 182 | }; | 279 | }; |
1154 | 183 | 280 | ||
1155 | 184 | /** | 281 | /** |
1157 | 185 | \brief Indicates an invalid parameter, such as a negative value when a positive one was | 282 | \brief Indicates an invalid parameter. |
1158 | 283 | |||
1159 | 284 | Use this exception for errors such as a negative parameter value when a positive one was | ||
1160 | 186 | expected, or a string that does not parse correctly or is empty when it should be non-empty. | 285 | expected, or a string that does not parse correctly or is empty when it should be non-empty. |
1161 | 286 | \note Do <i>not</i> throw this exception to indicate incorrect use of the API. | ||
1162 | 287 | \see LogicException | ||
1163 | 187 | */ | 288 | */ |
1164 | 289 | |||
1165 | 188 | class UNITY_STORAGE_EXPORT InvalidArgumentException : public StorageException | 290 | class UNITY_STORAGE_EXPORT InvalidArgumentException : public StorageException |
1166 | 189 | { | 291 | { |
1167 | 190 | public: | 292 | public: |
1168 | 293 | /** | ||
1169 | 294 | \brief Construct an InvalidArgumentException. | ||
1170 | 295 | \param error_message The error message for the exception. | ||
1171 | 296 | */ | ||
1172 | 191 | InvalidArgumentException(std::string const& error_message); | 297 | InvalidArgumentException(std::string const& error_message); |
1173 | 192 | ~InvalidArgumentException(); | 298 | ~InvalidArgumentException(); |
1174 | 193 | }; | 299 | }; |
1175 | 194 | 300 | ||
1176 | 195 | /** | 301 | /** |
1179 | 196 | \brief Indicates a system error, such as failure to create a file or folder, | 302 | \brief Indicates a system error. |
1180 | 197 | or any other (usually non-recoverable) kind of error that should not arise during normal operation. | 303 | |
1181 | 304 | This is a generic "catch-all" exception that you can throw to indicate unexpected errors that do not fit into | ||
1182 | 305 | any of the other categories. For example, ResourceException is appropriate to indicate out of | ||
1183 | 306 | file descriptors, failure to locate a configuration file, or any other unexpected system error. You should | ||
1184 | 307 | provide as much contextual information about such errors as possible. In particular, unexpected errors | ||
1185 | 308 | typically need to be diagnosed from log files. This means that you should provide, at least, the full error | ||
1186 | 309 | message you received from the cloud service (or the operating system), together with all other relevant | ||
1187 | 310 | details, such as the name of the file, the URL of a failed request, any HTTP error information, and so on. | ||
1188 | 198 | */ | 311 | */ |
1189 | 312 | |||
1190 | 199 | class UNITY_STORAGE_EXPORT ResourceException : public StorageException | 313 | class UNITY_STORAGE_EXPORT ResourceException : public StorageException |
1191 | 200 | { | 314 | { |
1192 | 201 | public: | 315 | public: |
1193 | 316 | /** | ||
1194 | 317 | \brief Construct an InvalidArgumentException. | ||
1195 | 318 | \param error_message The error message for the exception. | ||
1196 | 319 | \param error_code The system (or library) error code for the exception. | ||
1197 | 320 | In case of system call failures, <code>error_code</code> should be the value of <code>errno</code>. | ||
1198 | 321 | If the error code represents something else, such as a <code>QFileDevice::FileError</code>, | ||
1199 | 322 | also indicate in the error message what that error code is; otherwise, it can be | ||
1200 | 323 | impossible to diagnose a problem, especially when looking at log files after the fact. | ||
1201 | 324 | */ | ||
1202 | 202 | ResourceException(std::string const& error_message, int error_code); | 325 | ResourceException(std::string const& error_message, int error_code); |
1203 | 203 | ~ResourceException(); | 326 | ~ResourceException(); |
1204 | 204 | 327 | ||
1205 | 328 | /** | ||
1206 | 329 | \brief Return the error code. | ||
1207 | 330 | \return The value of the <code>error_code</code> parameter that was passed to the constructor. | ||
1208 | 331 | */ | ||
1209 | 205 | int error_code() const noexcept; | 332 | int error_code() const noexcept; |
1210 | 206 | 333 | ||
1211 | 207 | private: | 334 | private: |
1212 | @@ -209,12 +336,25 @@ | |||
1213 | 209 | }; | 336 | }; |
1214 | 210 | 337 | ||
1215 | 211 | /** | 338 | /** |
1218 | 212 | \brief Indicates that the server side caught an exception that does not derive from | 339 | \brief Indicates that a provider threw an exception not derived from StorageException. |
1219 | 213 | StorageException, such as a std::exception, or caught some other unknown type (such as `int`). | 340 | |
1220 | 341 | The runtime translates all exception that are thrown by a provider and that do not derive from StorageException | ||
1221 | 342 | to UnknownException. | ||
1222 | 343 | |||
1223 | 344 | Do not throw this exception explicitly; is is a "catch-all" exception that the runtime uses to | ||
1224 | 345 | indicate that something completely unexpected happened. (If a client receives this exception from your | ||
1225 | 346 | provider, chances are that you have forgotten to handle an error condition somewhere.) | ||
1226 | 214 | */ | 347 | */ |
1227 | 348 | |||
1228 | 215 | class UNITY_STORAGE_EXPORT UnknownException : public StorageException | 349 | class UNITY_STORAGE_EXPORT UnknownException : public StorageException |
1229 | 216 | { | 350 | { |
1230 | 217 | public: | 351 | public: |
1231 | 352 | /** | ||
1232 | 353 | \brief Construct an UnknownArgumentException. | ||
1233 | 354 | \param error_message The error message for the exception. This is the string returned by <code>what()</code> | ||
1234 | 355 | for <code>std::exception</code> or "Unknown exception" for exceptions that do not derive | ||
1235 | 356 | from <code>std::exception</code>. | ||
1236 | 357 | */ | ||
1237 | 218 | UnknownException(std::string const& error_message); | 358 | UnknownException(std::string const& error_message); |
1238 | 219 | ~UnknownException(); | 359 | ~UnknownException(); |
1239 | 220 | }; | 360 | }; |
1240 | 221 | 361 | ||
1241 | === modified file 'include/unity/storage/provider/Item.h' | |||
1242 | --- include/unity/storage/provider/Item.h 2017-03-17 02:51:05 +0000 | |||
1243 | +++ include/unity/storage/provider/Item.h 2017-04-06 07:42:38 +0000 | |||
1244 | @@ -33,17 +33,63 @@ | |||
1245 | 33 | namespace provider | 33 | namespace provider |
1246 | 34 | { | 34 | { |
1247 | 35 | 35 | ||
1248 | 36 | /** | ||
1249 | 37 | \brief The type of a metadata item (key–value pair). | ||
1250 | 38 | |||
1251 | 39 | For boolean values, use <code>int64_t</code> with a zero or non-zero value to | ||
1252 | 40 | indicate <code>false</code> or <code>true</code>. | ||
1253 | 41 | |||
1254 | 42 | Well-known metadata keys are defined in \ref common.h. | ||
1255 | 43 | */ | ||
1256 | 36 | // Note: When growing the set of supported variant types, add new types | 44 | // Note: When growing the set of supported variant types, add new types |
1257 | 37 | // to the *end* of the list, and update the marshaling code in dbusmarshal.cpp. | 45 | // to the *end* of the list, and update the marshaling code in dbusmarshal.cpp. |
1258 | 38 | typedef boost::variant<std::string, int64_t> MetadataValue; | 46 | typedef boost::variant<std::string, int64_t> MetadataValue; |
1259 | 39 | 47 | ||
1260 | 48 | /** | ||
1261 | 49 | \brief Details about a storage item. | ||
1262 | 50 | */ | ||
1263 | 51 | |||
1264 | 40 | struct UNITY_STORAGE_EXPORT Item | 52 | struct UNITY_STORAGE_EXPORT Item |
1265 | 41 | { | 53 | { |
1266 | 54 | /** | ||
1267 | 55 | \brief The unique identity of the item. | ||
1268 | 56 | |||
1269 | 57 | See <a href="index.html#identity">File and Folder Identity</a> for detailed semantics. | ||
1270 | 58 | */ | ||
1271 | 42 | std::string item_id; | 59 | std::string item_id; |
1272 | 60 | |||
1273 | 61 | /** | ||
1274 | 62 | \brief The list of parent folder identities. | ||
1275 | 63 | |||
1276 | 64 | \note Depending on the provider, it is possible for a file or folder to have more than one | ||
1277 | 65 | parent folder. | ||
1278 | 66 | */ | ||
1279 | 43 | std::vector<std::string> parent_ids; | 67 | std::vector<std::string> parent_ids; |
1280 | 68 | |||
1281 | 69 | /** | ||
1282 | 70 | \brief The name of a file or folder. | ||
1283 | 71 | |||
1284 | 72 | \note A provider may create a file or folder with a name other than the name that was requested by the | ||
1285 | 73 | client (such as folding upper case to lower case). This field contains the name of the item as created | ||
1286 | 74 | by the provider, not the name requested by the client. | ||
1287 | 75 | */ | ||
1288 | 44 | std::string name; | 76 | std::string name; |
1289 | 77 | |||
1290 | 78 | /** | ||
1291 | 79 | \brief The ETag of a file. | ||
1292 | 80 | |||
1293 | 81 | \note For folders, the ETag is may be empty because not all providers support ETags for folders. | ||
1294 | 82 | */ | ||
1295 | 45 | std::string etag; | 83 | std::string etag; |
1296 | 84 | |||
1297 | 85 | /** | ||
1298 | 86 | \brief The item type (file, folder, or root,). | ||
1299 | 87 | */ | ||
1300 | 46 | unity::storage::ItemType type; | 88 | unity::storage::ItemType type; |
1301 | 89 | |||
1302 | 90 | /** | ||
1303 | 91 | \brief Additional metadata for a file or folder. | ||
1304 | 92 | */ | ||
1305 | 47 | std::map<std::string, MetadataValue> metadata; | 93 | std::map<std::string, MetadataValue> metadata; |
1306 | 48 | }; | 94 | }; |
1307 | 49 | 95 | ||
1308 | 50 | 96 | ||
1309 | === modified file 'include/unity/storage/provider/ProviderBase.h' | |||
1310 | --- include/unity/storage/provider/ProviderBase.h 2017-03-16 02:50:03 +0000 | |||
1311 | +++ include/unity/storage/provider/ProviderBase.h 2017-04-06 07:42:38 +0000 | |||
1312 | @@ -42,15 +42,41 @@ | |||
1313 | 42 | class DownloadJob; | 42 | class DownloadJob; |
1314 | 43 | class UploadJob; | 43 | class UploadJob; |
1315 | 44 | 44 | ||
1316 | 45 | /** | ||
1317 | 46 | \brief Security related information for an operation invocation. | ||
1318 | 47 | |||
1319 | 48 | Each provider method has a trailing parameter of type Context that provides | ||
1320 | 49 | access to | ||
1321 | 50 | <a href="https://help.ubuntu.com/lts/serverguide/apparmor.html">Apparmor</a> details as | ||
1322 | 51 | well as credentials for the cloud provider. | ||
1323 | 52 | */ | ||
1324 | 53 | |||
1325 | 45 | struct UNITY_STORAGE_EXPORT Context | 54 | struct UNITY_STORAGE_EXPORT Context |
1326 | 46 | { | 55 | { |
1330 | 47 | uid_t uid; | 56 | uid_t uid; /*!< The user ID of the client process. */ |
1331 | 48 | pid_t pid; | 57 | pid_t pid; /*!< The process ID of the client process. */ |
1332 | 49 | std::string security_label; | 58 | std::string security_label; /*!< The Apparmor security label of the client process. */ |
1333 | 50 | 59 | ||
1335 | 51 | Credentials credentials; | 60 | Credentials credentials; /*!< Credentials to authenticate with the cloud provider. */ |
1336 | 52 | }; | 61 | }; |
1337 | 53 | 62 | ||
1338 | 63 | /** | ||
1339 | 64 | \brief Abstract base class for provider implementations. | ||
1340 | 65 | |||
1341 | 66 | The runtime calls methods on this class in response to incoming requests from clients. Each method | ||
1342 | 67 | implements a specific operation on the storage backend. | ||
1343 | 68 | |||
1344 | 69 | All methods are called from the main thread and must not block. Methods indicate error conditions to the runtime | ||
1345 | 70 | by throwing exceptions (which are caught and handled by the runtime). Alternatively, methods can return a future | ||
1346 | 71 | that stores an exception to indicate an error. | ||
1347 | 72 | |||
1348 | 73 | \note Besides the explicitly listed exceptions for specific error conditions, all methods | ||
1349 | 74 | must indicate other errors by throwing an exception derived from StorageException, such | ||
1350 | 75 | as PermissionException or ResourceException (see <a href="index.html#provider-error-handling">Error Handling</a>). | ||
1351 | 76 | If an exception that does not derive from StorageException is thrown a provider method, the runtime returns a | ||
1352 | 77 | generic UnknownException to the client. | ||
1353 | 78 | */ | ||
1354 | 79 | |||
1355 | 54 | class UNITY_STORAGE_EXPORT ProviderBase : public std::enable_shared_from_this<ProviderBase> | 80 | class UNITY_STORAGE_EXPORT ProviderBase : public std::enable_shared_from_this<ProviderBase> |
1356 | 55 | { | 81 | { |
1357 | 56 | public: | 82 | public: |
1358 | @@ -60,41 +86,201 @@ | |||
1359 | 60 | ProviderBase(ProviderBase const& other) = delete; | 86 | ProviderBase(ProviderBase const& other) = delete; |
1360 | 61 | ProviderBase& operator=(ProviderBase const& other) = delete; | 87 | ProviderBase& operator=(ProviderBase const& other) = delete; |
1361 | 62 | 88 | ||
1397 | 63 | virtual boost::future<ItemList> roots(std::vector<std::string> const& keys, Context const& context) = 0; | 89 | /** |
1398 | 64 | virtual boost::future<std::tuple<ItemList,std::string>> list( | 90 | \brief Return the root folder (or folders) of the provider. |
1399 | 65 | std::string const& item_id, std::string const& page_token, | 91 | \param keys The keys of metadata items that the client wants to receive. |
1400 | 66 | std::vector<std::string> const& keys, | 92 | \param context The security context of the operation. |
1401 | 67 | Context const& context) = 0; | 93 | \return A (non-empty) list of roots. |
1402 | 68 | virtual boost::future<ItemList> lookup( | 94 | */ |
1403 | 69 | std::string const& parent_id, std::string const& name, std::vector<std::string> const& keys, | 95 | virtual boost::future<ItemList> roots(std::vector<std::string> const& keys, |
1404 | 70 | Context const& context) = 0; | 96 | Context const& context) = 0; |
1405 | 71 | virtual boost::future<Item> metadata(std::string const& item_id, std::vector<std::string> const& keys, | 97 | |
1406 | 72 | Context const& context) = 0; | 98 | /** |
1407 | 73 | 99 | \brief Return a (non-recursive) list of the contents of a folder. | |
1408 | 74 | virtual boost::future<Item> create_folder( | 100 | \param item_id The identity of the folder. |
1409 | 75 | std::string const& parent_id, std::string const& name, std::vector<std::string> const& keys, | 101 | \param page_token A token identifying the next page of results (empty for the initial request). |
1410 | 76 | Context const& context) = 0; | 102 | \param keys The keys of metadata items that the client wants to receive. |
1411 | 77 | 103 | \param context The security context of the operation. | |
1412 | 78 | virtual boost::future<std::unique_ptr<UploadJob>> create_file( | 104 | \return A tuple containing a number of child items, plus a new page token. The page token |
1413 | 79 | std::string const& parent_id, std::string const& name, | 105 | allows the method to return the results in multiple "pages". For the initial request, the runtime passes |
1414 | 80 | int64_t size, std::string const& content_type, bool allow_overwrite, std::vector<std::string> const& keys, | 106 | an empty <code>page_token</code>. The method returns new page token to indicate whether there are more |
1415 | 81 | Context const& context) = 0; | 107 | results to be retrieved. If the returned token is non-empty, the runtime calls the method again, |
1416 | 82 | virtual boost::future<std::unique_ptr<UploadJob>> update( | 108 | passing the token that was returned by the previous call. To indicate that all results were retrieved, the method |
1417 | 83 | std::string const& item_id, int64_t size, std::string const& old_etag, std::vector<std::string> const& keys, | 109 | must return an empty page token to the runtime. |
1418 | 84 | Context const& context) = 0; | 110 | \throws InvalidArgumentException <code>item_id</code> or <code>page_token</code> are invalid. |
1419 | 85 | 111 | \throws NotExistsException <code>item_id</code> does not exist. | |
1420 | 86 | virtual boost::future<std::unique_ptr<DownloadJob>> download( | 112 | \throws LogicException If at all possible, the implementation should throw a LogicException |
1421 | 87 | std::string const& item_id, std::string const& match_etag, | 113 | if <code>item_id</code> denotes a file. If a provider cannot distinguish between |
1422 | 88 | Context const& context) = 0; | 114 | attempts to list a file and other errors, it <i>must</i> return an empty list instead. |
1423 | 89 | 115 | */ | |
1424 | 90 | virtual boost::future<void> delete_item( | 116 | virtual boost::future<std::tuple<ItemList,std::string>> list(std::string const& item_id, |
1425 | 91 | std::string const& item_id, Context const& context) = 0; | 117 | std::string const& page_token, |
1426 | 92 | virtual boost::future<Item> move( | 118 | std::vector<std::string> const& keys, |
1427 | 93 | std::string const& item_id, std::string const& new_parent_id, | 119 | Context const& context) = 0; |
1428 | 94 | std::string const& new_name, std::vector<std::string> const& keys, Context const& context) = 0; | 120 | |
1429 | 95 | virtual boost::future<Item> copy( | 121 | /** |
1430 | 96 | std::string const& item_id, std::string const& new_parent_id, | 122 | \brief Retrieve a file or folder within a parent folder by name. |
1431 | 97 | std::string const& new_name, std::vector<std::string> const& keys, Context const& context) = 0; | 123 | \param parent_id The identity of the parent folder. |
1432 | 124 | \param name The name of the file or folder. | ||
1433 | 125 | \param keys The keys of metadata items that the client wants to receive. | ||
1434 | 126 | \param context The security context of the operation. | ||
1435 | 127 | \return A non-empty list of items with the given name. The list may contain more than one entry | ||
1436 | 128 | if the provider allows non-unique names for items within a folder. | ||
1437 | 129 | The list may also contain more than one entry with the same identity if the provider allows | ||
1438 | 130 | the same item to have more than one name. | ||
1439 | 131 | \throws InvalidArgumentException <code>parent_id</code> or <code>name</code> are invalid. | ||
1440 | 132 | \throws NotExistsException <code>parent_id</code> does not exist or does not contain a file or folder with | ||
1441 | 133 | the given <code>name</code>. (Do <i>not</i> return an empty list in this case.) | ||
1442 | 134 | */ | ||
1443 | 135 | virtual boost::future<ItemList> lookup(std::string const& parent_id, | ||
1444 | 136 | std::string const& name, | ||
1445 | 137 | std::vector<std::string> const& keys, | ||
1446 | 138 | Context const& context) = 0; | ||
1447 | 139 | |||
1448 | 140 | /** | ||
1449 | 141 | \brief Retrieve a file or folder by its identity. | ||
1450 | 142 | \param item_id The identity of the item. | ||
1451 | 143 | \param keys The keys of metadata items that the client wants to receive. | ||
1452 | 144 | \param context The security context of the operation. | ||
1453 | 145 | \return The item. | ||
1454 | 146 | \throws InvalidArgumentException <code>item_id</code> is invalid. | ||
1455 | 147 | \throws NotExistsException No file or folder with the given identity exists. | ||
1456 | 148 | */ | ||
1457 | 149 | virtual boost::future<Item> metadata(std::string const& item_id, | ||
1458 | 150 | std::vector<std::string> const& keys, | ||
1459 | 151 | Context const& context) = 0; | ||
1460 | 152 | |||
1461 | 153 | /** | ||
1462 | 154 | \brief Create a new folder. | ||
1463 | 155 | \param parent_id The identity of the parent folder. | ||
1464 | 156 | \param name The name of the new folder. | ||
1465 | 157 | \param keys The keys of metadata items that the client wants to receive. | ||
1466 | 158 | \param context The security context of the operation. | ||
1467 | 159 | \return The item representing the folder. | ||
1468 | 160 | \throws InvalidArgumentException <code>parent_id</code> or <code>name</code> are invalid. | ||
1469 | 161 | \throws ExistsException An item with the given <code>name</code> exists already. | ||
1470 | 162 | */ | ||
1471 | 163 | virtual boost::future<Item> create_folder(std::string const& parent_id, | ||
1472 | 164 | std::string const& name, | ||
1473 | 165 | std::vector<std::string> const& keys, | ||
1474 | 166 | Context const& context) = 0; | ||
1475 | 167 | |||
1476 | 168 | /** | ||
1477 | 169 | \brief Create a new file. | ||
1478 | 170 | \param parent_id The identity of the parent folder. | ||
1479 | 171 | \param name The name of the new file. | ||
1480 | 172 | \param size The size of the file contents in bytes. | ||
1481 | 173 | \param content_type The mime type of the file. If empty, the provider may (or may not) determine the mime type automatically. | ||
1482 | 174 | \param allow_overwrite If true, the file will be created even if a file with the same name exists already. | ||
1483 | 175 | \param keys The keys of metadata items that the client wants to receive. | ||
1484 | 176 | \param context The security context of the operation. | ||
1485 | 177 | \return An UploadJob that will read any data provided by the client and write it to the new file. | ||
1486 | 178 | \throws InvalidArgumentException <code>parent_id</code> or <code>name</code> are invalid. | ||
1487 | 179 | \throws ExistsException A file with the given <code>name</code> exists already and | ||
1488 | 180 | <code>allow_overwrite</code> is <code>false</code>, or a folder with the given <code>name</code> exists already. | ||
1489 | 181 | */ | ||
1490 | 182 | // TODO: The runtime should check that size is non-negative, so the provider implementation can rely on this. | ||
1491 | 183 | virtual boost::future<std::unique_ptr<UploadJob>> create_file(std::string const& parent_id, | ||
1492 | 184 | std::string const& name, | ||
1493 | 185 | int64_t size, | ||
1494 | 186 | std::string const& content_type, | ||
1495 | 187 | bool allow_overwrite, | ||
1496 | 188 | std::vector<std::string> const& keys, | ||
1497 | 189 | Context const& context) = 0; | ||
1498 | 190 | |||
1499 | 191 | /** | ||
1500 | 192 | \brief Update the contents of a file. | ||
1501 | 193 | \param item_id The identity of the file. | ||
1502 | 194 | \param size The size of the file contents in bytes. | ||
1503 | 195 | \param old_etag The ETag of the existing file (empty if the file should be overwritten). | ||
1504 | 196 | \param keys The keys of metadata items that the client wants to receive. | ||
1505 | 197 | \param context The security context of the operation. | ||
1506 | 198 | \return An UploadJob that will read any data provided by the client and write it to the file. | ||
1507 | 199 | \throws InvalidArgumentException <code>item_id</code> or <code>size</code> are invalid. | ||
1508 | 200 | \throws ConflictException The file's ETag does not match the given (non-empty) <code>old_etag</code>. | ||
1509 | 201 | \throws LogicException The <code>item_id</code> denotes a folder. | ||
1510 | 202 | */ | ||
1511 | 203 | // TODO: The runtime should check that size is non-negative, so the provider implementation can rely on this. | ||
1512 | 204 | virtual boost::future<std::unique_ptr<UploadJob>> update(std::string const& item_id, | ||
1513 | 205 | int64_t size, | ||
1514 | 206 | std::string const& old_etag, | ||
1515 | 207 | std::vector<std::string> const& keys, | ||
1516 | 208 | Context const& context) = 0; | ||
1517 | 209 | |||
1518 | 210 | /** | ||
1519 | 211 | \brief Download the contents of a file. | ||
1520 | 212 | \param item_id The identity of the file. | ||
1521 | 213 | \param match_etag The ETag of the existing file (empty if the file should be downloaded unconditionally). | ||
1522 | 214 | \param context The security context of the operation. | ||
1523 | 215 | \return A DownloadJob that will write the file data to the client socket. | ||
1524 | 216 | \throws InvalidArgumentException <code>item_id</code> or <code>name</code> are invalid. | ||
1525 | 217 | \throws NotExistsException <code>item_id</code> does not exist. | ||
1526 | 218 | \throws LogicException The <code>item_id</code> denotes a folder. | ||
1527 | 219 | \throws ConflictException The ETag for <code>item_id</code> does not match the given | ||
1528 | 220 | (non-empty) <code>match_etag</code>. | ||
1529 | 221 | */ | ||
1530 | 222 | virtual boost::future<std::unique_ptr<DownloadJob>> download(std::string const& item_id, | ||
1531 | 223 | std::string const& match_etag, | ||
1532 | 224 | Context const& context) = 0; | ||
1533 | 225 | |||
1534 | 226 | /** | ||
1535 | 227 | \brief Delete an item. | ||
1536 | 228 | |||
1537 | 229 | Deletion is recursive. If <code>item_id</code> denotes a folder, the folder and all its contents are deleted. | ||
1538 | 230 | \param item_id The identity of the file or folder. | ||
1539 | 231 | \param context The security context of the operation. | ||
1540 | 232 | \throws InvalidArgumentException <code>item_id</code> is invalid. | ||
1541 | 233 | \throws PermissionException <code>item_id</code> denotes a root (or permission was denied for another reason). | ||
1542 | 234 | */ | ||
1543 | 235 | virtual boost::future<void> delete_item(std::string const& item_id, | ||
1544 | 236 | Context const& context) = 0; | ||
1545 | 237 | |||
1546 | 238 | /** | ||
1547 | 239 | \brief Move and/or rename an item. | ||
1548 | 240 | |||
1549 | 241 | \param item_id The identity of the file or folder to be moved. | ||
1550 | 242 | \param new_parent_id The identity of the folder to move the file or folder to. If <code>new_parent_id</code> | ||
1551 | 243 | is the same as the existing parent of <code>item_id</code>, the file or folder is to be renamed within its | ||
1552 | 244 | parent folder. | ||
1553 | 245 | \param new_name The new name of the file (which can be the same as the old name if <code>new_parent_id</code> | ||
1554 | 246 | differs). | ||
1555 | 247 | \param keys The keys of metadata items that the client wants to receive. | ||
1556 | 248 | \param context The security context of the operation. | ||
1557 | 249 | \return The moved or renamed item. | ||
1558 | 250 | \throws InvalidArgumentException <code>item_id</code>, <code>new_parent_id</code>, or <code>new_name</code> | ||
1559 | 251 | are invalid. | ||
1560 | 252 | \throws ExistsException The folder <code>new_parent_id</code> already contains an item with | ||
1561 | 253 | name <code>new_name</code>. | ||
1562 | 254 | \throws PermissionException <code>item_id</code> denotes a root (or permission was denied for another reason). | ||
1563 | 255 | */ | ||
1564 | 256 | virtual boost::future<Item> move(std::string const& item_id, | ||
1565 | 257 | std::string const& new_parent_id, | ||
1566 | 258 | std::string const& new_name, | ||
1567 | 259 | std::vector<std::string> const& keys, | ||
1568 | 260 | Context const& context) = 0; | ||
1569 | 261 | |||
1570 | 262 | /** | ||
1571 | 263 | \brief Copy an item. | ||
1572 | 264 | |||
1573 | 265 | Copy is recursive, so copying a folder copies the folder's contents. | ||
1574 | 266 | \param item_id The identity of the file or folder to be copied. | ||
1575 | 267 | \param new_parent_id The identity of the folder to copy the file or folder to. If <code>new_parent_id</code> | ||
1576 | 268 | is the same as the existing parent of <code>item_id</code>, the file or folder is to be copied within its | ||
1577 | 269 | parent folder. | ||
1578 | 270 | \param new_name The new name of the file or folder. | ||
1579 | 271 | \param keys The keys of metadata items that the client wants to receive. | ||
1580 | 272 | \param context The security context of the operation. | ||
1581 | 273 | \return The copied item. | ||
1582 | 274 | \throws InvalidArgumentException <code>item_id</code>, <code>new_parent_id</code>, or <code>new_name</code> | ||
1583 | 275 | are invalid. | ||
1584 | 276 | \throws ExistsException The folder <code>new_parent_id</code> already contains an item with | ||
1585 | 277 | name <code>new_name</code>. | ||
1586 | 278 | */ | ||
1587 | 279 | virtual boost::future<Item> copy(std::string const& item_id, | ||
1588 | 280 | std::string const& new_parent_id, | ||
1589 | 281 | std::string const& new_name, | ||
1590 | 282 | std::vector<std::string> const& keys, | ||
1591 | 283 | Context const& context) = 0; | ||
1592 | 98 | }; | 284 | }; |
1593 | 99 | 285 | ||
1594 | 100 | } | 286 | } |
1595 | 101 | 287 | ||
1596 | === modified file 'include/unity/storage/provider/Server.h' | |||
1597 | --- include/unity/storage/provider/Server.h 2017-03-16 03:30:18 +0000 | |||
1598 | +++ include/unity/storage/provider/Server.h 2017-04-06 07:42:38 +0000 | |||
1599 | @@ -37,29 +37,141 @@ | |||
1600 | 37 | 37 | ||
1601 | 38 | class ProviderBase; | 38 | class ProviderBase; |
1602 | 39 | 39 | ||
1603 | 40 | /** | ||
1604 | 41 | \brief Base class to register a storage provider with the runtime. | ||
1605 | 42 | |||
1606 | 43 | \anchor extra-args This class allows you to register a provider that requires additional | ||
1607 | 44 | parameters with the storage framework runtime, by overriding the | ||
1608 | 45 | make_provider() method. (If your provider does not require constructor | ||
1609 | 46 | arguments, use the predefined Server template instead.) | ||
1610 | 47 | |||
1611 | 48 | For example, here is how you can instantiate a provider that requires | ||
1612 | 49 | a <code>some_value</code> constructor argument: | ||
1613 | 50 | |||
1614 | 51 | \code{.cpp} | ||
1615 | 52 | class MyCloudProvider : public ProviderBase | ||
1616 | 53 | { | ||
1617 | 54 | public: | ||
1618 | 55 | MyCloudProvider(int some_value, | ||
1619 | 56 | string const& bus_name, | ||
1620 | 57 | string const& service_id) | ||
1621 | 58 | : ProviderBase(bus_name, service_id) | ||
1622 | 59 | { | ||
1623 | 60 | } | ||
1624 | 61 | // ... | ||
1625 | 62 | }; | ||
1626 | 63 | |||
1627 | 64 | class MyCloudServer : public ServerBase | ||
1628 | 65 | { | ||
1629 | 66 | public: | ||
1630 | 67 | MyCloudServer(int some_value, | ||
1631 | 68 | string const& bus_name, | ||
1632 | 69 | string const& account_service_id) | ||
1633 | 70 | : some_value_(some_value) | ||
1634 | 71 | , bus_name_(bus_name) | ||
1635 | 72 | , account_service_id_(account_service_id) | ||
1636 | 73 | { | ||
1637 | 74 | } | ||
1638 | 75 | |||
1639 | 76 | protected: | ||
1640 | 77 | shared_ptr<ProviderBase> make_provider() override | ||
1641 | 78 | { | ||
1642 | 79 | return make_shared<MyCloudProvider>(some_value, bus_name, account_service_id); | ||
1643 | 80 | } | ||
1644 | 81 | |||
1645 | 82 | private: | ||
1646 | 83 | int some_value_; | ||
1647 | 84 | string bus_name_; | ||
1648 | 85 | string account_service_id_; | ||
1649 | 86 | }; | ||
1650 | 87 | |||
1651 | 88 | int main(int argc, char* argv[]) | ||
1652 | 89 | { | ||
1653 | 90 | string const bus_name = "com.acme.StorageFramework.Provider.MyCloud"; | ||
1654 | 91 | string const account_service_id = "storage-provider-mycloud"; | ||
1655 | 92 | |||
1656 | 93 | try | ||
1657 | 94 | { | ||
1658 | 95 | MyCloudServer server(99, bus_name, account_service_id); | ||
1659 | 96 | server.init(argc, argv); | ||
1660 | 97 | return server.run(); | ||
1661 | 98 | } | ||
1662 | 99 | catch (std::exception const& e) | ||
1663 | 100 | { | ||
1664 | 101 | cerr << argv[0] << ": " << e.what() << endl; | ||
1665 | 102 | return 1; | ||
1666 | 103 | } | ||
1667 | 104 | } | ||
1668 | 105 | \endcode | ||
1669 | 106 | */ | ||
1670 | 107 | |||
1671 | 40 | class UNITY_STORAGE_EXPORT ServerBase | 108 | class UNITY_STORAGE_EXPORT ServerBase |
1672 | 41 | { | 109 | { |
1673 | 42 | public: | 110 | public: |
1674 | 111 | /** | ||
1675 | 112 | \brief Constructs a server instance. | ||
1676 | 113 | \param bus_name The DBus name of the provider service on the session bus. | ||
1677 | 114 | \param account_service_id The service ID with which the provider is known to | ||
1678 | 115 | <a href="https://help.ubuntu.com/stable/ubuntu-help/accounts.html">Online Accounts</a>. | ||
1679 | 116 | */ | ||
1680 | 43 | ServerBase(std::string const& bus_name, std::string const& account_service_id); | 117 | ServerBase(std::string const& bus_name, std::string const& account_service_id); |
1681 | 44 | virtual ~ServerBase(); | 118 | virtual ~ServerBase(); |
1682 | 45 | 119 | ||
1683 | 120 | /** | ||
1684 | 121 | \brief Initializes the storage framework runtime. | ||
1685 | 122 | \param argc, argv The runtime passes these parameters through to | ||
1686 | 123 | <a href="http://doc.qt.io/qt-5/qcoreapplication.html">QCoreApplication</a>. | ||
1687 | 124 | \throws StorageException | ||
1688 | 125 | */ | ||
1689 | 46 | void init(int& argc, char** argv); | 126 | void init(int& argc, char** argv); |
1690 | 127 | |||
1691 | 128 | /** | ||
1692 | 129 | \brief Starts an event loop for the service. | ||
1693 | 130 | |||
1694 | 131 | You <i>must</i> call init() before calling this method. | ||
1695 | 132 | \return In case of an error, run() returns non-zero status. | ||
1696 | 133 | */ | ||
1697 | 47 | int run(); | 134 | int run(); |
1698 | 48 | 135 | ||
1699 | 49 | protected: | 136 | protected: |
1700 | 137 | /** | ||
1701 | 138 | \brief Factory method to instantiate a provider. | ||
1702 | 139 | The runtime calls this method to instantiate a provider as part | ||
1703 | 140 | of the init() method. You can override this method if you need | ||
1704 | 141 | to pass additional arguments to the constructor of your provider class. | ||
1705 | 142 | (see \ref extra-args "example code"). | ||
1706 | 143 | \return A <code>shared_ptr</code> to the provider instance. | ||
1707 | 144 | */ | ||
1708 | 50 | virtual std::shared_ptr<ProviderBase> make_provider() = 0; | 145 | virtual std::shared_ptr<ProviderBase> make_provider() = 0; |
1709 | 146 | |||
1710 | 51 | private: | 147 | private: |
1711 | 52 | std::unique_ptr<internal::ServerImpl> p_; | 148 | std::unique_ptr<internal::ServerImpl> p_; |
1712 | 53 | 149 | ||
1713 | 54 | friend class internal::ServerImpl; | 150 | friend class internal::ServerImpl; |
1714 | 55 | }; | 151 | }; |
1715 | 56 | 152 | ||
1716 | 153 | /** | ||
1717 | 154 | \brief Default ServerBase implementation for providers with a default constructor. | ||
1718 | 155 | |||
1719 | 156 | You can use this class to connect your provider class to the runtime, provided | ||
1720 | 157 | the class has a default constructor. If you need to pass additional arguments | ||
1721 | 158 | to the constructor, you must derive a class from ServerBase and override the | ||
1722 | 159 | ServerBase::make_provider() factory method (see \ref extra-args "example code"). | ||
1723 | 160 | */ | ||
1724 | 161 | |||
1725 | 57 | template <typename T> | 162 | template <typename T> |
1726 | 58 | class Server : public ServerBase | 163 | class Server : public ServerBase |
1727 | 59 | { | 164 | { |
1728 | 60 | public: | 165 | public: |
1729 | 61 | using ServerBase::ServerBase; | 166 | using ServerBase::ServerBase; |
1730 | 167 | |||
1731 | 62 | protected: | 168 | protected: |
1732 | 169 | /** | ||
1733 | 170 | \brief Factory method to instantiate a provider with a default constructor. | ||
1734 | 171 | |||
1735 | 172 | The runtime calls this method to create your provider instance of type T. | ||
1736 | 173 | \return A <code>shared_ptr</code> to the provider instance. | ||
1737 | 174 | */ | ||
1738 | 63 | std::shared_ptr<ProviderBase> make_provider() override { | 175 | std::shared_ptr<ProviderBase> make_provider() override { |
1739 | 64 | return std::make_shared<T>(); | 176 | return std::make_shared<T>(); |
1740 | 65 | } | 177 | } |
1741 | 66 | 178 | ||
1742 | === modified file 'include/unity/storage/provider/TempfileUploadJob.h' | |||
1743 | --- include/unity/storage/provider/TempfileUploadJob.h 2016-08-24 10:52:17 +0000 | |||
1744 | +++ include/unity/storage/provider/TempfileUploadJob.h 2017-04-06 07:42:38 +0000 | |||
1745 | @@ -36,21 +36,59 @@ | |||
1746 | 36 | class TempfileUploadJobImpl; | 36 | class TempfileUploadJobImpl; |
1747 | 37 | } | 37 | } |
1748 | 38 | 38 | ||
1749 | 39 | /** | ||
1750 | 40 | \brief Helper class to store the contents of an upload in a temporary file. | ||
1751 | 41 | |||
1752 | 42 | For uploads, a provider implementation must decide whether to upload data to the cloud provider | ||
1753 | 43 | immediately (as soon as the data arrives from the client) or whether to buffer the data locally first | ||
1754 | 44 | and upload it to the cloud provider once the client successfully finalizes the upload. Uploading | ||
1755 | 45 | immediately has the down side that it is more likely to fail. This is an issue particularly | ||
1756 | 46 | for mobile devices, where applications may be suspended for extended periods, and where connectivity | ||
1757 | 47 | is often lost without warning. Uploads of large files (such as media files) are unlikely | ||
1758 | 48 | to ever complete in this case: if the user swipes away from the client application, no more data arrives | ||
1759 | 49 | at the provider until the application resumes, by which time the connection to the cloud provider has | ||
1760 | 50 | most likely timed out. | ||
1761 | 51 | |||
1762 | 52 | Especially for files that are more than a few kilobytes in size, it is usually necessary to write the data | ||
1763 | 53 | to a local file first and to write it to the cloud provider only once all of data has been sent by the client. | ||
1764 | 54 | (The provider service does not get suspended and will not exit while an upload is in progress.) | ||
1765 | 55 | |||
1766 | 56 | TempfileUploadJob is a helper class that implements an uploader that writes the data to a temporary file. | ||
1767 | 57 | The file is unlinked once the TempfileUploadJob is destroyed. You implementation must provide finish() and | ||
1768 | 58 | cancel(). | ||
1769 | 59 | |||
1770 | 60 | */ | ||
1771 | 61 | |||
1772 | 39 | class UNITY_STORAGE_EXPORT TempfileUploadJob : public UploadJob | 62 | class UNITY_STORAGE_EXPORT TempfileUploadJob : public UploadJob |
1773 | 40 | { | 63 | { |
1774 | 41 | public: | 64 | public: |
1775 | 65 | /** | ||
1776 | 66 | \brief Construct a TempfileUploadJob. | ||
1777 | 67 | \param upload_id An identifier for this particular upload. You can use any non-empty string, | ||
1778 | 68 | as long as it is unique among all uploads that are in progress within this provider. | ||
1779 | 69 | The runtime uses the <code>upload_id</code> to distinguish different uploads that may be | ||
1780 | 70 | in progress concurrently. | ||
1781 | 71 | */ | ||
1782 | 42 | TempfileUploadJob(std::string const& upload_id); | 72 | TempfileUploadJob(std::string const& upload_id); |
1783 | 43 | virtual ~TempfileUploadJob(); | 73 | virtual ~TempfileUploadJob(); |
1784 | 44 | 74 | ||
1785 | 75 | /** | ||
1786 | 76 | \brief Returns the name of the file. | ||
1787 | 77 | \return The full path name of the temporary file. | ||
1788 | 78 | */ | ||
1789 | 45 | std::string file_name() const; | 79 | std::string file_name() const; |
1790 | 46 | 80 | ||
1795 | 47 | // This function should be called from your finish() | 81 | /** |
1796 | 48 | // implementation to read the remaining data from the socket. If | 82 | \brief Reads any unread data from the upload socket and writes it to the temporary file. |
1797 | 49 | // the client has not closed the socket as expected, LogicError | 83 | |
1798 | 50 | // will be thrown. | 84 | You must call drain() from your UploadJob::finish() method. Its implementation reads any remaining |
1799 | 85 | data from the upload socket and writes it to the temporary file. If an error occurs, | ||
1800 | 86 | drain() throws an exception. | ||
1801 | 87 | \throws LogicException The client did not close its end of the socket. | ||
1802 | 88 | */ | ||
1803 | 51 | void drain(); | 89 | void drain(); |
1804 | 52 | 90 | ||
1806 | 53 | protected: | 91 | private: |
1807 | 54 | TempfileUploadJob(internal::TempfileUploadJobImpl *p) UNITY_STORAGE_HIDDEN; | 92 | TempfileUploadJob(internal::TempfileUploadJobImpl *p) UNITY_STORAGE_HIDDEN; |
1808 | 55 | }; | 93 | }; |
1809 | 56 | 94 | ||
1810 | 57 | 95 | ||
1811 | === modified file 'include/unity/storage/provider/UploadJob.h' | |||
1812 | --- include/unity/storage/provider/UploadJob.h 2016-08-24 10:52:17 +0000 | |||
1813 | +++ include/unity/storage/provider/UploadJob.h 2017-04-06 07:42:38 +0000 | |||
1814 | @@ -39,28 +39,126 @@ | |||
1815 | 39 | class UploadJobImpl; | 39 | class UploadJobImpl; |
1816 | 40 | } | 40 | } |
1817 | 41 | 41 | ||
1818 | 42 | class TempfileUploadJob; | ||
1819 | 43 | |||
1820 | 44 | /** | ||
1821 | 45 | \brief Abstract base class for upload implementations. | ||
1822 | 46 | |||
1823 | 47 | When the runtime calls ProviderBase::update() or ProviderBase::create_file(), you must return | ||
1824 | 48 | a new uploader instance that derives from UploadJob. Your implementation is responsible | ||
1825 | 49 | for retrieving the file's data from the upload socket and writing it to | ||
1826 | 50 | the corresponding file in the cloud provider. | ||
1827 | 51 | |||
1828 | 52 | You can implement your uploader | ||
1829 | 53 | any way you wish, such as by running the upload in a separate thread, or | ||
1830 | 54 | by using async I/O driven by the runtime's (or any other) event loop. | ||
1831 | 55 | |||
1832 | 56 | The runtime invokes all methods on the uploader from the main thread. | ||
1833 | 57 | */ | ||
1834 | 58 | |||
1835 | 42 | class UNITY_STORAGE_EXPORT UploadJob | 59 | class UNITY_STORAGE_EXPORT UploadJob |
1836 | 43 | { | 60 | { |
1837 | 44 | public: | 61 | public: |
1838 | 62 | /** | ||
1839 | 63 | \brief Construct an uploader. | ||
1840 | 64 | \param upload_id An identifier for this particular upload. You can use any non-empty string, | ||
1841 | 65 | as long as it is unique among all uploads that are in progress within the corresponding account. | ||
1842 | 66 | (A simple incrementing counter will work fine, or you can use the upload identifier you receive | ||
1843 | 67 | from the cloud service.) | ||
1844 | 68 | The runtime uses the <code>upload_id</code> to distinguish different uploads that may be | ||
1845 | 69 | in progress concurrently, and it ensures that each ID can be used only by its corresponding client. | ||
1846 | 70 | */ | ||
1847 | 45 | UploadJob(std::string const& upload_id); | 71 | UploadJob(std::string const& upload_id); |
1848 | 46 | virtual ~UploadJob(); | 72 | virtual ~UploadJob(); |
1849 | 47 | 73 | ||
1850 | 74 | /** | ||
1851 | 75 | \brief Returns the upload ID. | ||
1852 | 76 | \return The value of the <code>upload_id</code> parameter that was passed to the constructor. | ||
1853 | 77 | */ | ||
1854 | 48 | std::string const& upload_id() const; | 78 | std::string const& upload_id() const; |
1855 | 79 | |||
1856 | 80 | /** | ||
1857 | 81 | \brief Returns the socket to read the file contents from. | ||
1858 | 82 | \return A socket that is open for reading. You must read the file contents from this socket (which is | ||
1859 | 83 | connected to the client by the runtime). | ||
1860 | 84 | */ | ||
1861 | 49 | int read_socket() const; | 85 | int read_socket() const; |
1862 | 50 | 86 | ||
1867 | 51 | // If an error is reported early, cancel() or finish() will not be | 87 | /** |
1868 | 52 | // invoked. | 88 | \brief Informs the runtime that an upload encountered an error. |
1869 | 53 | void report_error(std::exception_ptr p); | 89 | |
1870 | 54 | 90 | This method makes it unnecessary to wait for the runtime to call finish() in order to confirm | |
1871 | 91 | whether an upload completed successfully. You can call report_error() as soon as you encounter an error | ||
1872 | 92 | during an upload (such as losing the connection to the cloud provider, or getting an error when reading | ||
1873 | 93 | from the upload socket). This can be convenient if an upload runs in a separate thread or event loop | ||
1874 | 94 | because there is no need to store success/error state for use in finish(). | ||
1875 | 95 | |||
1876 | 96 | You can call report_error() from an arbitrary thread. | ||
1877 | 97 | |||
1878 | 98 | If you call report_error(), the runtime guarantees that neither finish() nor cancel() will be called, so | ||
1879 | 99 | you must reclaim any resources associated with the upload before calling report_error(). | ||
1880 | 100 | |||
1881 | 101 | \param storage_exception You <i>must</i> pass a StorageException to indicate the reason for the failure. | ||
1882 | 102 | \see finish() | ||
1883 | 103 | */ | ||
1884 | 104 | void report_error(std::exception_ptr storage_exception); | ||
1885 | 105 | |||
1886 | 106 | /** | ||
1887 | 107 | \brief Cancel this upload. | ||
1888 | 108 | |||
1889 | 109 | The runtime calls this method when a client explicitly cancels an upload or crashes. | ||
1890 | 110 | Your implementation should reclaim all resources that are used by the upload. In particular, | ||
1891 | 111 | you should stop reading any more data and close the upload socket. In addition, | ||
1892 | 112 | you should reclaim any resources (such as open connections) that are associated | ||
1893 | 113 | with the upload to the cloud provider (possibly after informing the cloud provider of | ||
1894 | 114 | the cancellation). | ||
1895 | 115 | |||
1896 | 116 | The runtime guarantees that cancel() will be called only once, and that finish() will not be called | ||
1897 | 117 | if cancel() was called. | ||
1898 | 118 | |||
1899 | 119 | If any errors are encountered, you <i>must</i> report them by returning a future that stores | ||
1900 | 120 | a StorageException. Do <i>not</i> call report_error() from inside cancel(). | ||
1901 | 121 | |||
1902 | 122 | \return A future that becomes ready (or contains a StorageException) once cancellation is complete. | ||
1903 | 123 | */ | ||
1904 | 55 | virtual boost::future<void> cancel() = 0; | 124 | virtual boost::future<void> cancel() = 0; |
1905 | 125 | |||
1906 | 126 | /** | ||
1907 | 127 | \brief Finalize this upload. | ||
1908 | 128 | |||
1909 | 129 | The runtime calls this method when a client finishes an upload. Your implementation <i>must</i> verify | ||
1910 | 130 | that it has successfully read <i>and</i> written the <i>exact</i> number of bytes that were passed | ||
1911 | 131 | to ProviderBase::update() or ProviderBase::create_file() before making the future ready. | ||
1912 | 132 | If too few or too many bytes were sent by the client, it <i>must</i> store a LogicException in the future. | ||
1913 | 133 | It must also verify that the cloud provider has received <i>all</i> of the file's data; otherwise, | ||
1914 | 134 | the file may end up being partially written in the cloud provider. | ||
1915 | 135 | |||
1916 | 136 | Note that finish() may be called while there is still buffered data that remains to be read from the | ||
1917 | 137 | upload socket, so you must take care to drain the socket of any remaining data before checking that the | ||
1918 | 138 | actual number bytes uploaded by the client matches the expected number of bytes. | ||
1919 | 139 | |||
1920 | 140 | You should close the upload socket and reclaim any resources (such as open | ||
1921 | 141 | connections) that are associated with the upload from the cloud provider. | ||
1922 | 142 | |||
1923 | 143 | The runtime guarantees that finish() will be called only once, and that cancel() will not be called | ||
1924 | 144 | if finish() was called. | ||
1925 | 145 | |||
1926 | 146 | If any errors are encountered, you <i>must</i> report them by returning a future that stores | ||
1927 | 147 | a StorageException. Do <i>not</i> call report_error() from inside finish(). | ||
1928 | 148 | |||
1929 | 149 | \return A future that becomes ready and contains the metadata for the file (or contains a StorageException) | ||
1930 | 150 | once the upload is complete. | ||
1931 | 151 | \see report_error(), TempfileUploadJob | ||
1932 | 152 | */ | ||
1933 | 56 | virtual boost::future<Item> finish() = 0; | 153 | virtual boost::future<Item> finish() = 0; |
1934 | 57 | 154 | ||
1936 | 58 | protected: | 155 | private: |
1937 | 59 | UploadJob(internal::UploadJobImpl *p) UNITY_STORAGE_HIDDEN; | 156 | UploadJob(internal::UploadJobImpl *p) UNITY_STORAGE_HIDDEN; |
1938 | 60 | internal::UploadJobImpl *p_ = nullptr; | 157 | internal::UploadJobImpl *p_ = nullptr; |
1939 | 61 | 158 | ||
1940 | 62 | friend class internal::PendingJobs; | 159 | friend class internal::PendingJobs; |
1941 | 63 | friend class internal::ProviderInterface; | 160 | friend class internal::ProviderInterface; |
1942 | 161 | friend class TempfileUploadJob; | ||
1943 | 64 | }; | 162 | }; |
1944 | 65 | 163 | ||
1945 | 66 | } | 164 | } |
1946 | 67 | 165 | ||
1947 | === modified file 'include/unity/storage/provider/testing/TestServer.h' | |||
1948 | --- include/unity/storage/provider/testing/TestServer.h 2016-09-28 07:04:41 +0000 | |||
1949 | +++ include/unity/storage/provider/testing/TestServer.h 2017-04-06 07:42:38 +0000 | |||
1950 | @@ -23,11 +23,13 @@ | |||
1951 | 23 | #include <memory> | 23 | #include <memory> |
1952 | 24 | #include <string> | 24 | #include <string> |
1953 | 25 | 25 | ||
1954 | 26 | //@cond | ||
1955 | 26 | namespace OnlineAccounts | 27 | namespace OnlineAccounts |
1956 | 27 | { | 28 | { |
1957 | 28 | class Account; | 29 | class Account; |
1958 | 29 | } | 30 | } |
1959 | 30 | class QDBusConnection; | 31 | class QDBusConnection; |
1960 | 32 | //@endcond | ||
1961 | 31 | 33 | ||
1962 | 32 | namespace unity | 34 | namespace unity |
1963 | 33 | { | 35 | { |
1964 | @@ -46,16 +48,44 @@ | |||
1965 | 46 | namespace testing | 48 | namespace testing |
1966 | 47 | { | 49 | { |
1967 | 48 | 50 | ||
1968 | 51 | /** | ||
1969 | 52 | \brief Helper class to enable testing of provider implementations. | ||
1970 | 53 | |||
1971 | 54 | |||
1972 | 55 | TestServer is a simple helper class that allows you to test a provider implementation | ||
1973 | 56 | on a separate DBus connection. The class requires access to | ||
1974 | 57 | an <a href="https://help.ubuntu.com/stable/ubuntu-help/accounts.html">Online Accounts</a> | ||
1975 | 58 | service. If you do not want to test with the live Online Accounts service, you can pass | ||
1976 | 59 | <code>nullptr</code>, in which case your provider receives blank | ||
1977 | 60 | \link unity::storage::provider::Credentials Credentials\endlink. | ||
1978 | 61 | */ | ||
1979 | 62 | |||
1980 | 49 | class UNITY_STORAGE_EXPORT TestServer | 63 | class UNITY_STORAGE_EXPORT TestServer |
1981 | 50 | { | 64 | { |
1982 | 51 | public: | 65 | public: |
1983 | 66 | /** | ||
1984 | 67 | \brief Constructs a TestServer instance. | ||
1985 | 68 | \param provider The provider implementation to be tested. | ||
1986 | 69 | \param account The account for the provider (or <code>nullptr</code>). | ||
1987 | 70 | \param connection The DBus connection to connect the provider to. | ||
1988 | 71 | \param object_path The DBus object path for the provider interface. | ||
1989 | 72 | */ | ||
1990 | 52 | TestServer(std::shared_ptr<ProviderBase> const& provider, | 73 | TestServer(std::shared_ptr<ProviderBase> const& provider, |
1991 | 53 | OnlineAccounts::Account* account, | 74 | OnlineAccounts::Account* account, |
1992 | 54 | QDBusConnection const& connection, | 75 | QDBusConnection const& connection, |
1993 | 55 | std::string const& object_path); | 76 | std::string const& object_path); |
1994 | 56 | ~TestServer(); | 77 | ~TestServer(); |
1995 | 57 | 78 | ||
1996 | 79 | /** | ||
1997 | 80 | \brief Returns the DBus connection. | ||
1998 | 81 | \return The value of the <code>connection</code> parameter that was passed to the constructor. | ||
1999 | 82 | */ | ||
2000 | 58 | QDBusConnection const& connection() const; | 83 | QDBusConnection const& connection() const; |
2001 | 84 | |||
2002 | 85 | /** | ||
2003 | 86 | \brief Returns the object path. | ||
2004 | 87 | \return The value of the <code>object_path</code> parameter that was passed to the constructor. | ||
2005 | 88 | */ | ||
2006 | 59 | std::string const& object_path() const; | 89 | std::string const& object_path() const; |
2007 | 60 | 90 | ||
2008 | 61 | private: | 91 | private: |
2009 | 62 | 92 | ||
2010 | === modified file 'include/unity/storage/qt/internal/StorageErrorImpl.h' | |||
2011 | --- include/unity/storage/qt/internal/StorageErrorImpl.h 2016-10-13 06:48:11 +0000 | |||
2012 | +++ include/unity/storage/qt/internal/StorageErrorImpl.h 2017-04-06 07:42:38 +0000 | |||
2013 | @@ -63,6 +63,7 @@ | |||
2014 | 63 | static StorageError not_exists_error(QString const& msg, QString const& key); | 63 | static StorageError not_exists_error(QString const& msg, QString const& key); |
2015 | 64 | static StorageError exists_error(QString const& msg, QString const& item_id, QString const& item_name); | 64 | static StorageError exists_error(QString const& msg, QString const& item_id, QString const& item_name); |
2016 | 65 | static StorageError cancelled_error(QString const& msg); | 65 | static StorageError cancelled_error(QString const& msg); |
2017 | 66 | static StorageError permission_error(QString const& msg); | ||
2018 | 66 | static StorageError logic_error(QString const& msg); | 67 | static StorageError logic_error(QString const& msg); |
2019 | 67 | static StorageError invalid_argument_error(QString const& msg); | 68 | static StorageError invalid_argument_error(QString const& msg); |
2020 | 68 | static StorageError resource_error(QString const& msg, int error_code); | 69 | static StorageError resource_error(QString const& msg, int error_code); |
2021 | 69 | 70 | ||
2022 | === modified file 'src/local-provider/LocalDownloadJob.cpp' | |||
2023 | --- src/local-provider/LocalDownloadJob.cpp 2017-03-14 07:11:40 +0000 | |||
2024 | +++ src/local-provider/LocalDownloadJob.cpp 2017-04-06 07:42:38 +0000 | |||
2025 | @@ -46,7 +46,7 @@ | |||
2026 | 46 | auto st = status(item_id_); | 46 | auto st = status(item_id_); |
2027 | 47 | if (!is_regular_file(st)) | 47 | if (!is_regular_file(st)) |
2028 | 48 | { | 48 | { |
2030 | 49 | throw InvalidArgumentException(method + ": \"" + item_id_ + "\" is not a file"); | 49 | throw LogicException(method + ": \"" + item_id_ + "\" is not a file"); |
2031 | 50 | } | 50 | } |
2032 | 51 | } | 51 | } |
2033 | 52 | // LCOV_EXCL_START // Too small a window to hit with a test. | 52 | // LCOV_EXCL_START // Too small a window to hit with a test. |
2034 | @@ -60,7 +60,7 @@ | |||
2035 | 60 | int64_t mtime = get_mtime_nsecs(method, item_id_); | 60 | int64_t mtime = get_mtime_nsecs(method, item_id_); |
2036 | 61 | if (to_string(mtime) != match_etag) | 61 | if (to_string(mtime) != match_etag) |
2037 | 62 | { | 62 | { |
2039 | 63 | throw ConflictException(method + ": etag mismatch"); | 63 | throw ConflictException(method + ": ETag mismatch"); |
2040 | 64 | } | 64 | } |
2041 | 65 | } | 65 | } |
2042 | 66 | 66 | ||
2043 | 67 | 67 | ||
2044 | === modified file 'src/local-provider/LocalProvider.cpp' | |||
2045 | --- src/local-provider/LocalProvider.cpp 2017-03-23 06:55:00 +0000 | |||
2046 | +++ src/local-provider/LocalProvider.cpp 2017-04-06 07:42:38 +0000 | |||
2047 | @@ -195,6 +195,11 @@ | |||
2048 | 195 | using namespace boost::filesystem; | 195 | using namespace boost::filesystem; |
2049 | 196 | 196 | ||
2050 | 197 | This->throw_if_not_valid(method, item_id); | 197 | This->throw_if_not_valid(method, item_id); |
2051 | 198 | if (!is_directory(item_id)) | ||
2052 | 199 | { | ||
2053 | 200 | string msg = method + ": \"" + item_id + "\" is not a folder"; | ||
2054 | 201 | throw boost::enable_current_exception(LogicException(msg)); | ||
2055 | 202 | } | ||
2056 | 198 | vector<Item> items; | 203 | vector<Item> items; |
2057 | 199 | for (directory_iterator it(item_id); it != directory_iterator(); ++it) | 204 | for (directory_iterator it(item_id); it != directory_iterator(); ++it) |
2058 | 200 | { | 205 | { |
2059 | @@ -345,7 +350,7 @@ | |||
2060 | 345 | if (canonical(item_id).native() == This->root_) | 350 | if (canonical(item_id).native() == This->root_) |
2061 | 346 | { | 351 | { |
2062 | 347 | string msg = method + ": cannot delete root"; | 352 | string msg = method + ": cannot delete root"; |
2064 | 348 | throw boost::enable_current_exception(LogicException(msg)); | 353 | throw boost::enable_current_exception(PermissionException(msg)); |
2065 | 349 | } | 354 | } |
2066 | 350 | remove_all(item_id); | 355 | remove_all(item_id); |
2067 | 351 | }; | 356 | }; |
2068 | @@ -378,6 +383,11 @@ | |||
2069 | 378 | string msg = method + ": \"" + target_path.native() + "\" exists already"; | 383 | string msg = method + ": \"" + target_path.native() + "\" exists already"; |
2070 | 379 | throw boost::enable_current_exception(ExistsException(msg, target_path.native(), new_name)); | 384 | throw boost::enable_current_exception(ExistsException(msg, target_path.native(), new_name)); |
2071 | 380 | } | 385 | } |
2072 | 386 | if (canonical(item_id).native() == This->root_) | ||
2073 | 387 | { | ||
2074 | 388 | string msg = method + ": cannot move root"; | ||
2075 | 389 | throw boost::enable_current_exception(PermissionException(msg)); | ||
2076 | 390 | } | ||
2077 | 381 | 391 | ||
2078 | 382 | // Small race condition here: if exists() just said that the target does not exist, it is | 392 | // Small race condition here: if exists() just said that the target does not exist, it is |
2079 | 383 | // possible for it to have been created since. If so, if the target is a file or an empty | 393 | // possible for it to have been created since. If so, if the target is a file or an empty |
2080 | 384 | 394 | ||
2081 | === modified file 'src/local-provider/LocalUploadJob.cpp' | |||
2082 | --- src/local-provider/LocalUploadJob.cpp 2017-03-16 02:50:03 +0000 | |||
2083 | +++ src/local-provider/LocalUploadJob.cpp 2017-04-06 07:42:38 +0000 | |||
2084 | @@ -83,7 +83,7 @@ | |||
2085 | 83 | auto st = status(item_id); | 83 | auto st = status(item_id); |
2086 | 84 | if (!is_regular_file(st)) | 84 | if (!is_regular_file(st)) |
2087 | 85 | { | 85 | { |
2089 | 86 | throw InvalidArgumentException(method_ + ": \"" + item_id + "\" is not a file"); | 86 | throw LogicException(method_ + ": \"" + item_id + "\" is not a file"); |
2090 | 87 | } | 87 | } |
2091 | 88 | } | 88 | } |
2092 | 89 | // LCOV_EXCL_START | 89 | // LCOV_EXCL_START |
2093 | @@ -99,7 +99,7 @@ | |||
2094 | 99 | int64_t mtime = get_mtime_nsecs(method_, item_id); | 99 | int64_t mtime = get_mtime_nsecs(method_, item_id); |
2095 | 100 | if (to_string(mtime) != old_etag) | 100 | if (to_string(mtime) != old_etag) |
2096 | 101 | { | 101 | { |
2098 | 102 | throw ConflictException(method_ + ": etag mismatch"); | 102 | throw ConflictException(method_ + ": ETag mismatch"); |
2099 | 103 | } | 103 | } |
2100 | 104 | } | 104 | } |
2101 | 105 | old_etag_ = old_etag; | 105 | old_etag_ = old_etag; |
2102 | @@ -184,14 +184,13 @@ | |||
2103 | 184 | 184 | ||
2104 | 185 | try | 185 | try |
2105 | 186 | { | 186 | { |
2107 | 187 | // We check again for an etag mismatch or overwrite, in case the file was updated after the upload started. | 187 | // We check again for an ETag mismatch or overwrite, in case the file was updated after the upload started. |
2108 | 188 | if (!parent_id_.empty()) | 188 | if (!parent_id_.empty()) |
2109 | 189 | { | 189 | { |
2110 | 190 | // create_file() | 190 | // create_file() |
2111 | 191 | if (!allow_overwrite_ && boost::filesystem::exists(item_id_)) | 191 | if (!allow_overwrite_ && boost::filesystem::exists(item_id_)) |
2112 | 192 | { | 192 | { |
2113 | 193 | string msg = method_ + ": \"" + item_id_ + "\" exists already"; | 193 | string msg = method_ + ": \"" + item_id_ + "\" exists already"; |
2114 | 194 | boost::filesystem::path(item_id_).filename().native(); | ||
2115 | 195 | BOOST_THROW_EXCEPTION( | 194 | BOOST_THROW_EXCEPTION( |
2116 | 196 | ExistsException(msg, item_id_, boost::filesystem::path(item_id_).filename().native())); | 195 | ExistsException(msg, item_id_, boost::filesystem::path(item_id_).filename().native())); |
2117 | 197 | } | 196 | } |
2118 | @@ -199,10 +198,22 @@ | |||
2119 | 199 | else if (!old_etag_.empty()) | 198 | else if (!old_etag_.empty()) |
2120 | 200 | { | 199 | { |
2121 | 201 | // update() | 200 | // update() |
2122 | 201 | using namespace boost::filesystem; | ||
2123 | 202 | if (exists(item_id_) && !is_regular_file(item_id_)) | ||
2124 | 203 | { | ||
2125 | 204 | // Yes, ExistsException, not LogicException. A folder is in the way | ||
2126 | 205 | // and was created after the update started, meaning that the client correctly | ||
2127 | 206 | // started the operation with an existing file, but then the file was removed | ||
2128 | 207 | // and replaced with a folder with the same name. | ||
2129 | 208 | string msg = method_ + ": \"" + item_id_ + "\" exists already"; | ||
2130 | 209 | BOOST_THROW_EXCEPTION( | ||
2131 | 210 | ExistsException(msg, item_id_, boost::filesystem::path(item_id_).filename().native())); | ||
2132 | 211 | } | ||
2133 | 202 | int64_t mtime = get_mtime_nsecs(method_, item_id_); | 212 | int64_t mtime = get_mtime_nsecs(method_, item_id_); |
2134 | 203 | if (to_string(mtime) != old_etag_) | 213 | if (to_string(mtime) != old_etag_) |
2135 | 204 | { | 214 | { |
2137 | 205 | BOOST_THROW_EXCEPTION(ConflictException(method_ + ": etag mismatch")); | 215 | // File was touched after the upload started. |
2138 | 216 | BOOST_THROW_EXCEPTION(ConflictException(method_ + ": ETag mismatch")); | ||
2139 | 206 | } | 217 | } |
2140 | 207 | } | 218 | } |
2141 | 208 | 219 | ||
2142 | 209 | 220 | ||
2143 | === modified file 'src/qt/internal/ItemImpl.cpp' | |||
2144 | --- src/qt/internal/ItemImpl.cpp 2016-11-25 03:23:55 +0000 | |||
2145 | +++ src/qt/internal/ItemImpl.cpp 2017-04-06 07:42:38 +0000 | |||
2146 | @@ -235,7 +235,7 @@ | |||
2147 | 235 | } | 235 | } |
2148 | 236 | if (md_.type == storage::ItemType::root) | 236 | if (md_.type == storage::ItemType::root) |
2149 | 237 | { | 237 | { |
2151 | 238 | auto e = StorageErrorImpl::logic_error(method + ": cannot delete root"); | 238 | auto e = StorageErrorImpl::permission_error(method + ": cannot delete root"); |
2152 | 239 | return VoidJobImpl::make_job(e); | 239 | return VoidJobImpl::make_job(e); |
2153 | 240 | } | 240 | } |
2154 | 241 | 241 | ||
2155 | 242 | 242 | ||
2156 | === modified file 'src/qt/internal/StorageErrorImpl.cpp' | |||
2157 | --- src/qt/internal/StorageErrorImpl.cpp 2017-03-14 07:11:40 +0000 | |||
2158 | +++ src/qt/internal/StorageErrorImpl.cpp 2017-04-06 07:42:38 +0000 | |||
2159 | @@ -188,6 +188,12 @@ | |||
2160 | 188 | return StorageError(move(p)); | 188 | return StorageError(move(p)); |
2161 | 189 | } | 189 | } |
2162 | 190 | 190 | ||
2163 | 191 | StorageError StorageErrorImpl::permission_error(QString const& msg) | ||
2164 | 192 | { | ||
2165 | 193 | unique_ptr<StorageErrorImpl> p(new StorageErrorImpl(StorageError::Type::PermissionDenied, msg)); | ||
2166 | 194 | return StorageError(move(p)); | ||
2167 | 195 | } | ||
2168 | 196 | |||
2169 | 191 | StorageError StorageErrorImpl::logic_error(QString const& msg) | 197 | StorageError StorageErrorImpl::logic_error(QString const& msg) |
2170 | 192 | { | 198 | { |
2171 | 193 | unique_ptr<StorageErrorImpl> p(new StorageErrorImpl(StorageError::Type::LogicError, msg)); | 199 | unique_ptr<StorageErrorImpl> p(new StorageErrorImpl(StorageError::Type::LogicError, msg)); |
2172 | 194 | 200 | ||
2173 | === modified file 'src/qt/internal/unmarshal_error.cpp' | |||
2174 | --- src/qt/internal/unmarshal_error.cpp 2017-01-24 06:12:09 +0000 | |||
2175 | +++ src/qt/internal/unmarshal_error.cpp 2017-04-06 07:42:38 +0000 | |||
2176 | @@ -84,11 +84,11 @@ | |||
2177 | 84 | { "ConflictException", make_error<StorageError::Type::Conflict> }, | 84 | { "ConflictException", make_error<StorageError::Type::Conflict> }, |
2178 | 85 | { "UnauthorizedException", make_error<StorageError::Type::Unauthorized> }, | 85 | { "UnauthorizedException", make_error<StorageError::Type::Unauthorized> }, |
2179 | 86 | { "PermissionException", make_error<StorageError::Type::PermissionDenied> }, | 86 | { "PermissionException", make_error<StorageError::Type::PermissionDenied> }, |
2180 | 87 | { "QuotaException", make_error<StorageError::Type::QuotaExceeded> }, | ||
2181 | 87 | { "CancelledException", make_error<StorageError::Type::Cancelled> }, | 88 | { "CancelledException", make_error<StorageError::Type::Cancelled> }, |
2182 | 88 | { "LogicException", make_error<StorageError::Type::LogicError> }, | 89 | { "LogicException", make_error<StorageError::Type::LogicError> }, |
2183 | 89 | { "InvalidArgumentException", make_error<StorageError::Type::InvalidArgument> }, | 90 | { "InvalidArgumentException", make_error<StorageError::Type::InvalidArgument> }, |
2184 | 90 | { "ResourceException", make_error<StorageError::Type::ResourceError> }, | 91 | { "ResourceException", make_error<StorageError::Type::ResourceError> }, |
2185 | 91 | { "QuotaException", make_error<StorageError::Type::QuotaExceeded> }, | ||
2186 | 92 | { "UnknownException", make_error<StorageError::Type::LocalCommsError> } // Yes, LocalCommsError is intentional | 92 | { "UnknownException", make_error<StorageError::Type::LocalCommsError> } // Yes, LocalCommsError is intentional |
2187 | 93 | }; | 93 | }; |
2188 | 94 | 94 | ||
2189 | 95 | 95 | ||
2190 | === modified file 'tests/local-provider/local-provider_test.cpp' | |||
2191 | --- tests/local-provider/local-provider_test.cpp 2017-03-23 06:55:00 +0000 | |||
2192 | +++ tests/local-provider/local-provider_test.cpp 2017-04-06 07:42:38 +0000 | |||
2193 | @@ -141,6 +141,22 @@ | |||
2194 | 141 | "nulla pariatur. Excepteur sint occaecat cupidatat non proident, " | 141 | "nulla pariatur. Excepteur sint occaecat cupidatat non proident, " |
2195 | 142 | "sunt in culpa qui officia deserunt mollit anim id est laborum.\n"; | 142 | "sunt in culpa qui officia deserunt mollit anim id est laborum.\n"; |
2196 | 143 | 143 | ||
2197 | 144 | void make_hierarchy(string const& root_dir) | ||
2198 | 145 | { | ||
2199 | 146 | // Make a small tree so we have something to test against. | ||
2200 | 147 | ASSERT_EQ(0, mkdir((root_dir + "/a").c_str(), 0755)); | ||
2201 | 148 | ASSERT_EQ(0, mkdir((root_dir + "/a/b").c_str(), 0755)); | ||
2202 | 149 | string cmd = string("echo hello >") + root_dir + "/hello"; | ||
2203 | 150 | ASSERT_EQ(0, system(cmd.c_str())); | ||
2204 | 151 | cmd = string("echo text >") + root_dir + "/a/foo.txt"; | ||
2205 | 152 | ASSERT_EQ(0, system(cmd.c_str())); | ||
2206 | 153 | ASSERT_EQ(0, mknod((root_dir + "/a/pipe").c_str(), S_IFIFO | 06666, 0)); | ||
2207 | 154 | ASSERT_EQ(0, mkdir((root_dir + "/a/.storage-framework-").c_str(), 0755)); | ||
2208 | 155 | ASSERT_EQ(0, mkdir((root_dir + "/a/b/.storage-framework-").c_str(), 0755)); | ||
2209 | 156 | ASSERT_EQ(0, mknod((root_dir + "/a/b/pipe").c_str(), S_IFIFO | 06666, 0)); | ||
2210 | 157 | ASSERT_EQ(0, mkdir((root_dir + "/empty_dir").c_str(), 0755)); | ||
2211 | 158 | } | ||
2212 | 159 | |||
2213 | 144 | } // namespace | 160 | } // namespace |
2214 | 145 | 161 | ||
2215 | 146 | TEST(Directories, env_vars) | 162 | TEST(Directories, env_vars) |
2216 | @@ -307,6 +323,9 @@ | |||
2217 | 307 | ASSERT_EQ(ItemJob::Error, job->status()) << job->error().errorString().toStdString(); | 323 | ASSERT_EQ(ItemJob::Error, job->status()) << job->error().errorString().toStdString(); |
2218 | 308 | EXPECT_EQ(string("Exists: create_folder(): \"") + ROOT_DIR() + "/child\" exists already", | 324 | EXPECT_EQ(string("Exists: create_folder(): \"") + ROOT_DIR() + "/child\" exists already", |
2219 | 309 | job->error().errorString().toStdString()); | 325 | job->error().errorString().toStdString()); |
2220 | 326 | EXPECT_EQ(ROOT_DIR() + "/child", job->error().itemId().toStdString()); | ||
2221 | 327 | EXPECT_EQ(ROOT_DIR() + "/child", job->error().itemId().toStdString()); | ||
2222 | 328 | EXPECT_EQ("child", job->error().itemName().toStdString()); | ||
2223 | 310 | 329 | ||
2224 | 311 | // Again, without write permission on the root dir, to get coverage for a filesystem_error in invoke_async(). | 330 | // Again, without write permission on the root dir, to get coverage for a filesystem_error in invoke_async(). |
2225 | 312 | ASSERT_EQ(0, ::rmdir((ROOT_DIR() + "/child").c_str())); | 331 | ASSERT_EQ(0, ::rmdir((ROOT_DIR() + "/child").c_str())); |
2226 | @@ -363,15 +382,17 @@ | |||
2227 | 363 | // Client-side API does not allow us to try to delete the root, so we talk to the provider directly. | 382 | // Client-side API does not allow us to try to delete the root, so we talk to the provider directly. |
2228 | 364 | auto p = make_shared<LocalProvider>(); | 383 | auto p = make_shared<LocalProvider>(); |
2229 | 365 | 384 | ||
2230 | 385 | make_hierarchy(ROOT_DIR()); | ||
2231 | 386 | |||
2232 | 366 | auto fut = p->delete_item(ROOT_DIR(), provider::Context()); | 387 | auto fut = p->delete_item(ROOT_DIR(), provider::Context()); |
2233 | 367 | try | 388 | try |
2234 | 368 | { | 389 | { |
2235 | 369 | fut.get(); | 390 | fut.get(); |
2236 | 370 | FAIL(); | 391 | FAIL(); |
2237 | 371 | } | 392 | } |
2239 | 372 | catch (provider::LogicException const& e) | 393 | catch (provider::PermissionException const& e) |
2240 | 373 | { | 394 | { |
2242 | 374 | EXPECT_STREQ("LogicException: delete_item(): cannot delete root", e.what()); | 395 | EXPECT_STREQ("PermissionException: delete_item(): cannot delete root", e.what()); |
2243 | 375 | } | 396 | } |
2244 | 376 | } | 397 | } |
2245 | 377 | 398 | ||
2246 | @@ -435,6 +456,8 @@ | |||
2247 | 435 | EXPECT_EQ(string("NotExists: lookup(): \"") + ROOT_DIR() + "/child\": boost::filesystem::canonical: " | 456 | EXPECT_EQ(string("NotExists: lookup(): \"") + ROOT_DIR() + "/child\": boost::filesystem::canonical: " |
2248 | 436 | + "No such file or directory: \"" + ROOT_DIR() + "/child\"", | 457 | + "No such file or directory: \"" + ROOT_DIR() + "/child\"", |
2249 | 437 | job->error().errorString().toStdString()); | 458 | job->error().errorString().toStdString()); |
2250 | 459 | EXPECT_EQ(qt::StorageError::NotExists, job->error().type()); | ||
2251 | 460 | EXPECT_EQ(ROOT_DIR() + "/child", job->error().itemId().toStdString()); | ||
2252 | 438 | } | 461 | } |
2253 | 439 | 462 | ||
2254 | 440 | TEST_F(LocalProviderTest, list) | 463 | TEST_F(LocalProviderTest, list) |
2255 | @@ -472,19 +495,24 @@ | |||
2256 | 472 | EXPECT_EQ(5, child.metadata().size()); | 495 | EXPECT_EQ(5, child.metadata().size()); |
2257 | 473 | } | 496 | } |
2258 | 474 | 497 | ||
2260 | 475 | void make_hierarchy(string const& root_dir) | 498 | TEST_F(LocalProviderTest, list_file) |
2261 | 476 | { | 499 | { |
2273 | 477 | // Make a small tree so we have something to test with for move() and copy(). | 500 | // We can't try a list on a file via the client API, so we use the provider directly. |
2274 | 478 | ASSERT_EQ(0, mkdir((root_dir + "/a").c_str(), 0755)); | 501 | |
2275 | 479 | ASSERT_EQ(0, mkdir((root_dir + "/a/b").c_str(), 0755)); | 502 | auto p = make_shared<LocalProvider>(); |
2276 | 480 | string cmd = string("echo hello >") + root_dir + "/hello"; | 503 | |
2277 | 481 | ASSERT_EQ(0, system(cmd.c_str())); | 504 | make_hierarchy(ROOT_DIR()); |
2278 | 482 | cmd = string("echo text >") + root_dir + "/a/foo.txt"; | 505 | |
2279 | 483 | ASSERT_EQ(0, system(cmd.c_str())); | 506 | try |
2280 | 484 | ASSERT_EQ(0, mknod((root_dir + "/a/pipe").c_str(), S_IFIFO | 06666, 0)); | 507 | { |
2281 | 485 | ASSERT_EQ(0, mkdir((root_dir + "/a/.storage-framework-").c_str(), 0755)); | 508 | auto fut = p->list(ROOT_DIR() + "/hello", "", {}, provider::Context()); |
2282 | 486 | ASSERT_EQ(0, mkdir((root_dir + "/a/b/.storage-framework-").c_str(), 0755)); | 509 | fut.get(); |
2283 | 487 | ASSERT_EQ(0, mknod((root_dir + "/a/b/pipe").c_str(), S_IFIFO | 06666, 0)); | 510 | FAIL(); |
2284 | 511 | } | ||
2285 | 512 | catch (provider::LogicException const& e) | ||
2286 | 513 | { | ||
2287 | 514 | EXPECT_EQ(string("LogicException: list(): \"") + ROOT_DIR() + "/hello\" is not a folder", e.what()); | ||
2288 | 515 | } | ||
2289 | 488 | } | 516 | } |
2290 | 489 | 517 | ||
2291 | 490 | TEST_F(LocalProviderTest, move) | 518 | TEST_F(LocalProviderTest, move) |
2292 | @@ -571,6 +599,26 @@ | |||
2293 | 571 | job->error().errorString().toStdString()); | 599 | job->error().errorString().toStdString()); |
2294 | 572 | } | 600 | } |
2295 | 573 | 601 | ||
2296 | 602 | TEST_F(LocalProviderTest, move_root) | ||
2297 | 603 | { | ||
2298 | 604 | // We can't try to move a root via the client API, so we use the LocalDownloadJob directly. | ||
2299 | 605 | |||
2300 | 606 | auto p = make_shared<LocalProvider>(); | ||
2301 | 607 | |||
2302 | 608 | make_hierarchy(ROOT_DIR()); | ||
2303 | 609 | |||
2304 | 610 | auto fut = p->move(ROOT_DIR(), ROOT_DIR() + "/empty_dir", "moved_root", {}, provider::Context()); | ||
2305 | 611 | try | ||
2306 | 612 | { | ||
2307 | 613 | fut.get(); | ||
2308 | 614 | FAIL(); | ||
2309 | 615 | } | ||
2310 | 616 | catch (provider::PermissionException const& e) | ||
2311 | 617 | { | ||
2312 | 618 | EXPECT_STREQ("PermissionException: move(): cannot move root", e.what()); | ||
2313 | 619 | } | ||
2314 | 620 | } | ||
2315 | 621 | |||
2316 | 574 | TEST_F(LocalProviderTest, copy_file) | 622 | TEST_F(LocalProviderTest, copy_file) |
2317 | 575 | { | 623 | { |
2318 | 576 | using namespace unity::storage::qt; | 624 | using namespace unity::storage::qt; |
2319 | @@ -659,6 +707,44 @@ | |||
2320 | 659 | } | 707 | } |
2321 | 660 | } | 708 | } |
2322 | 661 | 709 | ||
2323 | 710 | TEST_F(LocalProviderTest, copy_root) | ||
2324 | 711 | { | ||
2325 | 712 | using namespace unity::storage::qt; | ||
2326 | 713 | using namespace boost::filesystem; | ||
2327 | 714 | |||
2328 | 715 | set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider)); | ||
2329 | 716 | |||
2330 | 717 | make_hierarchy(ROOT_DIR()); | ||
2331 | 718 | |||
2332 | 719 | auto root = get_root(acc_); | ||
2333 | 720 | |||
2334 | 721 | qt::Item empty_dir; | ||
2335 | 722 | { | ||
2336 | 723 | unique_ptr<ItemListJob> job(root.lookup("empty_dir")); | ||
2337 | 724 | auto items = get_items(job.get()); | ||
2338 | 725 | ASSERT_EQ(ItemListJob::Finished, job->status()) << job->error().errorString().toStdString(); | ||
2339 | 726 | ASSERT_EQ(1, items.size()); | ||
2340 | 727 | empty_dir = items.at(0); | ||
2341 | 728 | } | ||
2342 | 729 | |||
2343 | 730 | { | ||
2344 | 731 | unique_ptr<ItemJob> job(root.copy(empty_dir, "copied_root")); | ||
2345 | 732 | wait(job.get()); | ||
2346 | 733 | ASSERT_EQ(ItemJob::Finished, job->status()) << job->error().errorString().toStdString(); | ||
2347 | 734 | } | ||
2348 | 735 | |||
2349 | 736 | // Check that we only copied regular files and directories, but not a pipe or anything starting with | ||
2350 | 737 | // the temp file prefix. | ||
2351 | 738 | EXPECT_TRUE(exists(ROOT_DIR() + "/empty_dir/copied_root/a/b")); | ||
2352 | 739 | EXPECT_TRUE(exists(ROOT_DIR() + "/empty_dir/copied_root/hello")); | ||
2353 | 740 | EXPECT_TRUE(exists(ROOT_DIR() + "/empty_dir/copied_root/a/foo.txt")); | ||
2354 | 741 | EXPECT_TRUE(exists(ROOT_DIR() + "/empty_dir/copied_root/empty_dir")); | ||
2355 | 742 | EXPECT_FALSE(exists(ROOT_DIR() + "/empty_dir/copied_root/a/pipe")); | ||
2356 | 743 | EXPECT_FALSE(exists(ROOT_DIR() + "/empty_dir/copied_root/a/storage-framework-")); | ||
2357 | 744 | EXPECT_FALSE(exists(ROOT_DIR() + "/empty_dir/copied_root/a/b/pipe")); | ||
2358 | 745 | EXPECT_FALSE(exists(ROOT_DIR() + "/empty_dir/copied_root/a/b/storage-framework-")); | ||
2359 | 746 | } | ||
2360 | 747 | |||
2361 | 662 | TEST_F(LocalProviderTest, download) | 748 | TEST_F(LocalProviderTest, download) |
2362 | 663 | { | 749 | { |
2363 | 664 | using namespace unity::storage::qt; | 750 | using namespace unity::storage::qt; |
2364 | @@ -785,7 +871,7 @@ | |||
2365 | 785 | 871 | ||
2366 | 786 | auto error = downloader->error(); | 872 | auto error = downloader->error(); |
2367 | 787 | EXPECT_EQ(qt::StorageError::Conflict, error.type()); | 873 | EXPECT_EQ(qt::StorageError::Conflict, error.type()); |
2369 | 788 | EXPECT_EQ("download(): etag mismatch", error.message().toStdString()); | 874 | EXPECT_EQ("download(): ETag mismatch", error.message().toStdString()); |
2370 | 789 | } | 875 | } |
2371 | 790 | 876 | ||
2372 | 791 | TEST_F(LocalProviderTest, download_wrong_file_type) | 877 | TEST_F(LocalProviderTest, download_wrong_file_type) |
2373 | @@ -802,9 +888,9 @@ | |||
2374 | 802 | LocalDownloadJob(p, dir, "some_etag"); | 888 | LocalDownloadJob(p, dir, "some_etag"); |
2375 | 803 | FAIL(); | 889 | FAIL(); |
2376 | 804 | } | 890 | } |
2378 | 805 | catch (provider::InvalidArgumentException const& e) | 891 | catch (provider::LogicException const& e) |
2379 | 806 | { | 892 | { |
2381 | 807 | EXPECT_EQ(string("InvalidArgumentException: download(): \"" + dir + "\" is not a file"), e.what()); | 893 | EXPECT_EQ(string("LogicException: download(): \"" + dir + "\" is not a file"), e.what()); |
2382 | 808 | } | 894 | } |
2383 | 809 | } | 895 | } |
2384 | 810 | 896 | ||
2385 | @@ -1013,7 +1099,7 @@ | |||
2386 | 1013 | { | 1099 | { |
2387 | 1014 | ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME)); | 1100 | ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME)); |
2388 | 1015 | } | 1101 | } |
2390 | 1016 | EXPECT_EQ("update(): etag mismatch", uploader->error().message().toStdString()); | 1102 | EXPECT_EQ("update(): ETag mismatch", uploader->error().message().toStdString()); |
2391 | 1017 | } | 1103 | } |
2392 | 1018 | 1104 | ||
2393 | 1019 | TEST_F(LocalProviderTest, update_file_touched_while_uploading) | 1105 | TEST_F(LocalProviderTest, update_file_touched_while_uploading) |
2394 | @@ -1062,7 +1148,56 @@ | |||
2395 | 1062 | ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME)); | 1148 | ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME)); |
2396 | 1063 | } | 1149 | } |
2397 | 1064 | ASSERT_EQ(Uploader::Error, uploader->status()); | 1150 | ASSERT_EQ(Uploader::Error, uploader->status()); |
2399 | 1065 | EXPECT_EQ("update(): etag mismatch", uploader->error().message().toStdString()); | 1151 | EXPECT_EQ("update(): ETag mismatch", uploader->error().message().toStdString()); |
2400 | 1152 | } | ||
2401 | 1153 | |||
2402 | 1154 | TEST_F(LocalProviderTest, update_folder_created_while_uploading) | ||
2403 | 1155 | { | ||
2404 | 1156 | using namespace unity::storage::qt; | ||
2405 | 1157 | |||
2406 | 1158 | set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider)); | ||
2407 | 1159 | |||
2408 | 1160 | auto full_path = ROOT_DIR() + "/foo.txt"; | ||
2409 | 1161 | auto cmd = string("echo hello >") + full_path; | ||
2410 | 1162 | ASSERT_EQ(0, system(cmd.c_str())); | ||
2411 | 1163 | |||
2412 | 1164 | unique_ptr<ItemJob> job(acc_.get(QString::fromStdString(full_path))); | ||
2413 | 1165 | wait(job.get()); | ||
2414 | 1166 | EXPECT_TRUE(job->isValid()); | ||
2415 | 1167 | |||
2416 | 1168 | auto file = job->item(); | ||
2417 | 1169 | auto old_etag = file.etag(); | ||
2418 | 1170 | |||
2419 | 1171 | int const segments = 50; | ||
2420 | 1172 | unique_ptr<Uploader> uploader(file.createUploader(Item::ErrorIfConflict, file_contents.size() * segments)); | ||
2421 | 1173 | |||
2422 | 1174 | int count = 0; | ||
2423 | 1175 | QTimer timer; | ||
2424 | 1176 | timer.setSingleShot(false); | ||
2425 | 1177 | timer.setInterval(10); | ||
2426 | 1178 | QObject::connect(&timer, &QTimer::timeout, [&] { | ||
2427 | 1179 | uploader->write(&file_contents[0], file_contents.size()); | ||
2428 | 1180 | count++; | ||
2429 | 1181 | if (count == segments / 2) | ||
2430 | 1182 | { | ||
2431 | 1183 | sleep(1); | ||
2432 | 1184 | cmd = string("rm ") + full_path + "; mkdir " + full_path; | ||
2433 | 1185 | ASSERT_EQ(0, system(cmd.c_str())); | ||
2434 | 1186 | } | ||
2435 | 1187 | else if (count == segments) | ||
2436 | 1188 | { | ||
2437 | 1189 | uploader->close(); | ||
2438 | 1190 | } | ||
2439 | 1191 | }); | ||
2440 | 1192 | |||
2441 | 1193 | QSignalSpy spy(uploader.get(), &Uploader::statusChanged); | ||
2442 | 1194 | timer.start(); | ||
2443 | 1195 | while (uploader->status() != Uploader::Error) | ||
2444 | 1196 | { | ||
2445 | 1197 | ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME)); | ||
2446 | 1198 | } | ||
2447 | 1199 | ASSERT_EQ(Uploader::Error, uploader->status()); | ||
2448 | 1200 | EXPECT_EQ(string("update(): \"") + full_path + "\" exists already", uploader->error().message().toStdString()); | ||
2449 | 1066 | } | 1201 | } |
2450 | 1067 | 1202 | ||
2451 | 1068 | TEST_F(LocalProviderTest, update_ignore_etag_mismatch) | 1203 | TEST_F(LocalProviderTest, update_ignore_etag_mismatch) |
2452 | @@ -1208,9 +1343,9 @@ | |||
2453 | 1208 | LocalUploadJob(p, dir, 0, ""); | 1343 | LocalUploadJob(p, dir, 0, ""); |
2454 | 1209 | FAIL(); | 1344 | FAIL(); |
2455 | 1210 | } | 1345 | } |
2457 | 1211 | catch (provider::InvalidArgumentException const& e) | 1346 | catch (provider::LogicException const& e) |
2458 | 1212 | { | 1347 | { |
2460 | 1213 | EXPECT_EQ(string("InvalidArgumentException: update(): \"" + dir + "\" is not a file"), e.what()); | 1348 | EXPECT_EQ(string("LogicException: update(): \"" + dir + "\" is not a file"), e.what()); |
2461 | 1214 | } | 1349 | } |
2462 | 1215 | } | 1350 | } |
2463 | 1216 | 1351 | ||
2464 | 1217 | 1352 | ||
2465 | === modified file 'tests/remote-client/remote-client_test.cpp' | |||
2466 | --- tests/remote-client/remote-client_test.cpp 2017-01-12 06:02:30 +0000 | |||
2467 | +++ tests/remote-client/remote-client_test.cpp 2017-04-06 07:42:38 +0000 | |||
2468 | @@ -946,7 +946,7 @@ | |||
2469 | 946 | unique_ptr<VoidJob> j(item.deleteItem()); | 946 | unique_ptr<VoidJob> j(item.deleteItem()); |
2470 | 947 | EXPECT_FALSE(j->isValid()); | 947 | EXPECT_FALSE(j->isValid()); |
2471 | 948 | EXPECT_EQ(VoidJob::Status::Error, j->status()); | 948 | EXPECT_EQ(VoidJob::Status::Error, j->status()); |
2473 | 949 | EXPECT_EQ(StorageError::Type::LogicError, j->error().type()); | 949 | EXPECT_EQ(StorageError::Type::PermissionDenied, j->error().type()); |
2474 | 950 | 950 | ||
2475 | 951 | // Signal must be received. | 951 | // Signal must be received. |
2476 | 952 | QSignalSpy spy(j.get(), &VoidJob::statusChanged); | 952 | QSignalSpy spy(j.get(), &VoidJob::statusChanged); |
PASSED: Continuous integration, rev:127 /jenkins. canonical. com/unity- api-1/job/ lp-storage- framework- ci/275/ /jenkins. canonical. com/unity- api-1/job/ build/1886 /jenkins. canonical. com/unity- api-1/job/ build-0- fetch/1893 /jenkins. canonical. com/unity- api-1/job/ build-2- binpkg/ arch=amd64, release= xenial+ overlay/ 1675 /jenkins. canonical. com/unity- api-1/job/ build-2- binpkg/ arch=amd64, release= xenial+ overlay/ 1675/artifact/ output/ *zip*/output. zip /jenkins. canonical. com/unity- api-1/job/ build-2- binpkg/ arch=amd64, release= zesty/1675 /jenkins. canonical. com/unity- api-1/job/ build-2- binpkg/ arch=amd64, release= zesty/1675/ artifact/ output/ *zip*/output. zip /jenkins. canonical. com/unity- api-1/job/ build-2- binpkg/ arch=armhf, release= xenial+ overlay/ 1675 /jenkins. canonical. com/unity- api-1/job/ build-2- binpkg/ arch=armhf, release= xenial+ overlay/ 1675/artifact/ output/ *zip*/output. zip /jenkins. canonical. com/unity- api-1/job/ build-2- binpkg/ arch=armhf, release= zesty/1675 /jenkins. canonical. com/unity- api-1/job/ build-2- binpkg/ arch=armhf, release= zesty/1675/ artifact/ output/ *zip*/output. zip /jenkins. canonical. com/unity- api-1/job/ build-2- binpkg/ arch=i386, release= xenial+ overlay/ 1675 /jenkins. canonical. com/unity- api-1/job/ build-2- binpkg/ arch=i386, release= xenial+ overlay/ 1675/artifact/ output/ *zip*/output. zip /jenkins. canonical. com/unity- api-1/job/ build-2- binpkg/ arch=i386, release= zesty/1675 /jenkins. canonical. com/unity- api-1/job/ build-2- binpkg/ arch=i386, release= zesty/1675/ artifact/ output/ *zip*/output. zip
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
Click here to trigger a rebuild: /jenkins. canonical. com/unity- api-1/job/ lp-storage- framework- ci/275/ rebuild
https:/