Merge lp:~michihenning/storage-framework/local-provider into lp:storage-framework/devel

Proposed by Michi Henning
Status: Merged
Approved by: James Henstridge
Approved revision: 116
Merged at revision: 113
Proposed branch: lp:~michihenning/storage-framework/local-provider
Merge into: lp:storage-framework/devel
Diff against target: 3626 lines (+3397/-9)
23 files modified
CMakeLists.txt (+4/-1)
debian/control.in (+2/-1)
debian/storage-framework-registry.install (+2/-0)
include/unity/storage/internal/gobj_memory.h (+212/-0)
include/unity/storage/provider/Exceptions.h (+1/-1)
src/CMakeLists.txt (+1/-0)
src/local-provider/CMakeLists.txt (+43/-0)
src/local-provider/LocalDownloadJob.cpp (+179/-0)
src/local-provider/LocalDownloadJob.h (+54/-0)
src/local-provider/LocalProvider.cpp (+586/-0)
src/local-provider/LocalProvider.h (+84/-0)
src/local-provider/LocalUploadJob.cpp (+338/-0)
src/local-provider/LocalUploadJob.h (+79/-0)
src/local-provider/com.canonical.StorageFramework.Provider.Local.service.in (+3/-0)
src/local-provider/main.cpp (+52/-0)
src/local-provider/utils.cpp (+146/-0)
src/local-provider/utils.h (+45/-0)
src/qt/internal/RuntimeImpl.cpp (+4/-0)
src/qt/internal/StorageErrorImpl.cpp (+0/-6)
tests/CMakeLists.txt (+1/-0)
tests/local-provider/CMakeLists.txt (+18/-0)
tests/local-provider/local-provider_test.cpp (+1473/-0)
tests/utils/env_var_guard.h (+70/-0)
To merge this branch: bzr merge lp:~michihenning/storage-framework/local-provider
Reviewer Review Type Date Requested Status
James Henstridge Approve
unity-api-1-bot continuous-integration Approve
Review via email: mp+319773@code.launchpad.net

Commit message

Added proper local provider.

Description of the change

Added proper local provider. Still need to update the registry to return an account for the provider, but will do that in a separate MR.

To post a comment you must log in.
Revision history for this message
unity-api-1-bot (unity-api-1-bot) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
unity-api-1-bot (unity-api-1-bot) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
James Henstridge (jamesh) wrote :

Attached is a partial review covering just the provider code. I haven't looked at any of the test changes yet.

review: Needs Fixing
111. By Michi Henning

Review comments from James.

112. By Michi Henning

Fix debian install file.

Revision history for this message
Michi Henning (michihenning) wrote :

Thanks for the review! I've responded inline.

Revision history for this message
unity-api-1-bot (unity-api-1-bot) wrote :

PASSED: Continuous integration, rev:112
https://jenkins.canonical.com/unity-api-1/job/lp-storage-framework-ci/253/
Executed test runs:
    SUCCESS: https://jenkins.canonical.com/unity-api-1/job/build/1781
    SUCCESS: https://jenkins.canonical.com/unity-api-1/job/build-0-fetch/1788
    SUCCESS: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=amd64,release=xenial+overlay/1564
        deb: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=amd64,release=xenial+overlay/1564/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=amd64,release=zesty/1564
        deb: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=amd64,release=zesty/1564/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=armhf,release=xenial+overlay/1564
        deb: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=armhf,release=xenial+overlay/1564/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=armhf,release=zesty/1564
        deb: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=armhf,release=zesty/1564/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=i386,release=xenial+overlay/1564
        deb: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=i386,release=xenial+overlay/1564/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=i386,release=zesty/1564
        deb: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=i386,release=zesty/1564/artifact/output/*zip*/output.zip

Click here to trigger a rebuild:
https://jenkins.canonical.com/unity-api-1/job/lp-storage-framework-ci/253/rebuild

review: Approve (continuous-integration)
113. By Michi Henning

Using gobj_ptr now.

114. By Michi Henning

Pay attention to SNAP_USER_DATA, plus other minor changes discussed with James.

Revision history for this message
unity-api-1-bot (unity-api-1-bot) wrote :

PASSED: Continuous integration, rev:114
https://jenkins.canonical.com/unity-api-1/job/lp-storage-framework-ci/254/
Executed test runs:
    SUCCESS: https://jenkins.canonical.com/unity-api-1/job/build/1785
    SUCCESS: https://jenkins.canonical.com/unity-api-1/job/build-0-fetch/1792
    SUCCESS: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=amd64,release=xenial+overlay/1568
        deb: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=amd64,release=xenial+overlay/1568/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=amd64,release=zesty/1568
        deb: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=amd64,release=zesty/1568/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=armhf,release=xenial+overlay/1568
        deb: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=armhf,release=xenial+overlay/1568/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=armhf,release=zesty/1568
        deb: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=armhf,release=zesty/1568/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=i386,release=xenial+overlay/1568
        deb: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=i386,release=xenial+overlay/1568/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=i386,release=zesty/1568
        deb: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=i386,release=zesty/1568/artifact/output/*zip*/output.zip

Click here to trigger a rebuild:
https://jenkins.canonical.com/unity-api-1/job/lp-storage-framework-ci/254/rebuild

review: Approve (continuous-integration)
Revision history for this message
James Henstridge (jamesh) wrote :

Can't easily leave inline comments for a diff this big, so I'll just note them here:

[1]
storage-provider-local should not be in the libstorage-framework-provider-1 binary package: that package is "Multi-Arch: same" (meaning one copy of the package for each architecture on multi-arch) systems), when we want it in a "Multi-Arch: foreign" (meaning only one copy is needed, and it can be used to satisfy deps of other packages. Also, the local provider is not strictly needed by other providers using this library.

Perhaps the storage-framework-registry binary package would be a better home?

[2]
I don't see a D-Bus service activation file for the local provider. It should be included in the same binary package as the local provider.

[3]
This is a bit of an annoying one since it will be a lot of search and replace, but is STORAGE_FRAMEWORK_ROOT a good name for the environment variable? It is controlling one particular provider rather than the entire framework.

Looking at some of the other environment variables we've got, perhaps SF_LOCAL_PROVIDER_ROOT would fit the pattern better?

[4]
This can probably be put off post-merge: we should probably have the local-provider tests use a unique temporary directory for each run to ensure that it can't interfere with other tests.

This can wait though, since what you've got is fine for a single serial run of the tests.

[5]
I don't see anything in the storage-framework-registry to let clients discover the local provider. Should that be covered in this branch, or should it be handled in a follow up?

review: Needs Fixing
115. By Michi Henning

Renamed STORAGE_FRAMEWORK_ROOT to SF_LOCAL_PROVIDER_ROOT.
Added .service file and fixed debian packaging.

116. By Michi Henning

Updated debian Standards-Version.

Revision history for this message
Michi Henning (michihenning) wrote :

1: Fixed, thanks!
2: Blush... I plain forgot :-(
3: Done, wasn't a big deal, and I agree that this is a better name.
4: The test fixture removes the tree at the start of each test run.
5: I was going to do that in a separate MR.

Revision history for this message
unity-api-1-bot (unity-api-1-bot) wrote :

PASSED: Continuous integration, rev:116
https://jenkins.canonical.com/unity-api-1/job/lp-storage-framework-ci/263/
Executed test runs:
    SUCCESS: https://jenkins.canonical.com/unity-api-1/job/build/1808
    SUCCESS: https://jenkins.canonical.com/unity-api-1/job/build-0-fetch/1815
    SUCCESS: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=amd64,release=xenial+overlay/1591
        deb: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=amd64,release=xenial+overlay/1591/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=amd64,release=zesty/1591
        deb: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=amd64,release=zesty/1591/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=armhf,release=xenial+overlay/1591
        deb: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=armhf,release=xenial+overlay/1591/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=armhf,release=zesty/1591
        deb: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=armhf,release=zesty/1591/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=i386,release=xenial+overlay/1591
        deb: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=i386,release=xenial+overlay/1591/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=i386,release=zesty/1591
        deb: https://jenkins.canonical.com/unity-api-1/job/build-2-binpkg/arch=i386,release=zesty/1591/artifact/output/*zip*/output.zip

Click here to trigger a rebuild:
https://jenkins.canonical.com/unity-api-1/job/lp-storage-framework-ci/263/rebuild

review: Approve (continuous-integration)
Revision history for this message
James Henstridge (jamesh) wrote :

Looks good.

My point about the temporary directory is that it adds a dependency on a scarce resource when we don't need to. If I ran this test suite twice in parallel, it would fail because one instance would be deleting files the other was expecting to find. The same would happen if we had a test runner that could invoke tests in parallel.

It doesn't have much practical effect right now because we are running tests serially, but I like to avoid these kinds of dependencies when possible.

review: Approve
Revision history for this message
Michi Henning (michihenning) wrote :

Fair enough. I'll push a separate MR for it.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'CMakeLists.txt'
2--- CMakeLists.txt 2017-02-24 03:01:57 +0000
3+++ CMakeLists.txt 2017-03-17 05:04:57 +0000
4@@ -43,7 +43,7 @@
5 include_directories(include)
6 include_directories(${CMAKE_BINARY_DIR}/include) # For generated headers
7
8-set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC --std=c++11 -Wall -pedantic -Wextra -fvisibility=hidden")
9+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC --std=c++14 -Wall -pedantic -Wextra -fvisibility=hidden")
10
11 # Some additional warnings not included by the general flags set above.
12 set(EXTRA_C_WARNINGS "-Wcast-align -Wcast-qual -Wformat -Wredundant-decls -Wswitch-default")
13@@ -123,6 +123,7 @@
14
15 include(FindPkgConfig)
16 pkg_check_modules(APPARMOR_DEPS REQUIRED libapparmor)
17+pkg_check_modules(GIO_DEPS REQUIRED gio-2.0 gio-unix-2.0)
18 pkg_check_modules(GLIB_DEPS REQUIRED glib-2.0)
19 pkg_check_modules(ONLINEACCOUNTS_DEPS REQUIRED OnlineAccountsQt)
20
21@@ -137,6 +138,7 @@
22
23 enable_coverage_report(
24 TARGETS
25+ local-provider-lib
26 qt-client-lib-common
27 storage-framework-common-internal
28 storage-framework-qt-client
29@@ -145,6 +147,7 @@
30 sf-provider-objects
31 storage-framework-provider
32 storage-framework-registry
33+ storage-provider-local
34 FILTER
35 ${CMAKE_SOURCE_DIR}/tests/*
36 ${CMAKE_BINARY_DIR}/*
37
38=== modified file 'debian/control.in'
39--- debian/control.in 2016-12-12 02:18:21 +0000
40+++ debian/control.in 2017-03-17 05:04:57 +0000
41@@ -2,7 +2,7 @@
42 Section: libs
43 Priority: optional
44 Maintainer: Ubuntu Core Developers <ubuntu-devel-discuss@lists.ubuntu.com>
45-Standards-Version: 3.9.6
46+Standards-Version: 3.9.7
47 Build-Depends: cmake,
48 cmake-extras (>= 0.10),
49 debhelper (>= 9),
50@@ -58,6 +58,7 @@
51 online-accounts-daemon,
52 Description: Registry for the Storage Framework
53 DBus service that provides access to provider account information.
54+ Includes a local storage provider.
55
56 Package: libstorage-framework-qt-client-2-0
57 Architecture: any
58
59=== modified file 'debian/storage-framework-registry.install'
60--- debian/storage-framework-registry.install 2016-11-21 04:37:37 +0000
61+++ debian/storage-framework-registry.install 2017-03-17 05:04:57 +0000
62@@ -1,2 +1,4 @@
63 usr/lib/*/*/storage-framework-registry
64 usr/share/dbus-1/services/com.canonical.StorageFramework.Registry.service
65+usr/lib/*/*/storage-provider-local
66+usr/share/dbus-1/services/com.canonical.StorageFramework.Provider.Local.service
67
68=== added file 'include/unity/storage/internal/gobj_memory.h'
69--- include/unity/storage/internal/gobj_memory.h 1970-01-01 00:00:00 +0000
70+++ include/unity/storage/internal/gobj_memory.h 2017-03-17 05:04:57 +0000
71@@ -0,0 +1,212 @@
72+/*
73+ * Copyright (C) 2013 Canonical Ltd.
74+ *
75+ * This program is free software: you can redistribute it and/or modify
76+ * it under the terms of the GNU Lesser General Public License version 3 as
77+ * published by the Free Software Foundation.
78+ *
79+ * This program is distributed in the hope that it will be useful,
80+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
81+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
82+ * GNU Lesser General Public License for more details.
83+ *
84+ * You should have received a copy of the GNU Lesser General Public License
85+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
86+ *
87+ * Authored by: Jussi Pakkanen <jussi.pakkanen@canonical.com>
88+ */
89+
90+#pragma once
91+
92+#include <stdexcept>
93+
94+#pragma GCC diagnostic push
95+#pragma GCC diagnostic ignored "-Wold-style-cast"
96+#pragma GCC diagnostic ignored "-Wcast-qual"
97+#include <glib-object.h>
98+
99+namespace unity
100+{
101+
102+namespace storage
103+{
104+
105+namespace internal
106+{
107+
108+/**
109+ * This class is meant for automatically managing the lifetime of C objects derived
110+ * from gobject. Its API perfectly mirrors the API of unique_ptr except that you
111+ * can't define your own deleter function as it is always g_object_unref.
112+ *
113+ * API/ABI stability is not guaranteed. If you need to pass the object across an ABI
114+ * boundary, pass the plain gobject.
115+ *
116+ * This is how you would use gobj_ptr 99% of the time:
117+ *
118+ * gobj_ptr<GSomeType> o(g_some_type_new(...));
119+ *
120+ * More specifically, the object will decrement the gobject reference count
121+ * of the object it points to when it goes out of scope. It will never increment it.
122+ * Thus you should only assign to it when already holding a reference. gobj_ptr
123+ * will then take ownership of that particular reference.
124+ *
125+ * Floating gobjects can not be put in this container as they are meant to be put
126+ * into native gobject aware containers immediately upon construction. Trying to insert
127+ * a floating gobject into a gobj_ptr will throw an invalid_argument exception. To
128+ * prevent accidental memory leaks, the floating gobject is unreffed in this case.
129+ */
130+template <typename T>
131+class gobj_ptr final
132+{
133+private:
134+ T* u;
135+
136+ void validate_float(T* t)
137+ {
138+ if (t != nullptr && g_object_is_floating(G_OBJECT(t)))
139+ {
140+ // LCOV_EXCL_START // False negative from gcovr.
141+ throw std::invalid_argument("Tried to add a floating gobject into a gobj_ptr.");
142+ // LCOV_EXCL_STOP
143+ }
144+ }
145+
146+public:
147+ typedef T element_type;
148+ typedef T* pointer;
149+ typedef decltype(g_object_unref) deleter_type;
150+
151+ constexpr gobj_ptr() noexcept : u(nullptr)
152+ {
153+ }
154+ explicit gobj_ptr(T* t)
155+ : u(t)
156+ {
157+ // What should we do if validate throws? Unreffing unknown objs
158+ // is dodgy but not unreffing runs the risk of
159+ // memory leaks. Currently unrefs as u is destroyed
160+ // when this exception is thrown.
161+ validate_float(t);
162+ }
163+ constexpr gobj_ptr(std::nullptr_t) noexcept : u(nullptr){};
164+ gobj_ptr(gobj_ptr&& o) noexcept
165+ {
166+ u = o.u;
167+ o.u = nullptr;
168+ }
169+ gobj_ptr(const gobj_ptr& o)
170+ : u(nullptr)
171+ {
172+ *this = o;
173+ }
174+ gobj_ptr& operator=(const gobj_ptr& o)
175+ {
176+ if (o.u != nullptr)
177+ {
178+ g_object_ref(o.u);
179+ }
180+ reset(o.u);
181+ return *this;
182+ }
183+ ~gobj_ptr()
184+ {
185+ reset();
186+ }
187+
188+ deleter_type& get_deleter() noexcept
189+ {
190+ return g_object_unref;
191+ }
192+ deleter_type& get_deleter() const noexcept
193+ {
194+ return g_object_unref;
195+ }
196+
197+ void swap(gobj_ptr<T>& o) noexcept
198+ {
199+ T* tmp = u;
200+ u = o.u;
201+ o.u = tmp;
202+ }
203+ void reset(pointer p = pointer())
204+ {
205+ if (u != nullptr)
206+ {
207+ g_object_unref(G_OBJECT(u));
208+ u = nullptr;
209+ }
210+ // Same throw dilemma as in pointer constructor.
211+ u = p;
212+ validate_float(p);
213+ }
214+
215+ T* release() noexcept
216+ {
217+ T* r = u;
218+ u = nullptr;
219+ return r;
220+ }
221+ T* get() const noexcept
222+ {
223+ return u;
224+ }
225+
226+ T& operator*() const
227+ {
228+ return *u;
229+ }
230+ T* operator->() const noexcept
231+ {
232+ return u;
233+ }
234+ explicit operator bool() const noexcept
235+ {
236+ return u != nullptr;
237+ }
238+
239+ gobj_ptr& operator=(gobj_ptr&& o) noexcept
240+ {
241+ reset();
242+ u = o.u;
243+ o.u = nullptr;
244+ return *this;
245+ }
246+ gobj_ptr& operator=(std::nullptr_t) noexcept
247+ {
248+ reset();
249+ return *this;
250+ }
251+ bool operator==(const gobj_ptr<T>& o) const noexcept
252+ {
253+ return u == o.u;
254+ }
255+ bool operator!=(const gobj_ptr<T>& o) const noexcept
256+ {
257+ return u != o.u;
258+ }
259+ bool operator<(const gobj_ptr<T>& o) const noexcept
260+ {
261+ return u < o.u;
262+ }
263+ bool operator<=(const gobj_ptr<T>& o) const noexcept
264+ {
265+ return u <= o.u;
266+ }
267+ bool operator>(const gobj_ptr<T>& o) const noexcept
268+ {
269+ return u > o.u;
270+ }
271+ bool operator>=(const gobj_ptr<T>& o) const noexcept
272+ {
273+ return u >= o.u;
274+ }
275+};
276+
277+} // namespace internal
278+
279+} // namespace storage
280+
281+} // namespace unity
282+
283+#pragma GCC diagnostic pop
284
285=== modified file 'include/unity/storage/provider/Exceptions.h'
286--- include/unity/storage/provider/Exceptions.h 2017-01-27 09:28:03 +0000
287+++ include/unity/storage/provider/Exceptions.h 2017-03-17 05:04:57 +0000
288@@ -95,7 +95,7 @@
289 };
290
291 /**
292-\brief Indicates that an upload detected a version mismatch.
293+\brief Indicates that an upload or download detected a version mismatch.
294 */
295 class UNITY_STORAGE_EXPORT ConflictException : public StorageException
296 {
297
298=== modified file 'src/CMakeLists.txt'
299--- src/CMakeLists.txt 2016-11-07 04:49:34 +0000
300+++ src/CMakeLists.txt 2017-03-17 05:04:57 +0000
301@@ -2,3 +2,4 @@
302 add_subdirectory(provider)
303 add_subdirectory(qt)
304 add_subdirectory(registry)
305+add_subdirectory(local-provider)
306
307=== added directory 'src/local-provider'
308=== added file 'src/local-provider/CMakeLists.txt'
309--- src/local-provider/CMakeLists.txt 1970-01-01 00:00:00 +0000
310+++ src/local-provider/CMakeLists.txt 2017-03-17 05:04:57 +0000
311@@ -0,0 +1,43 @@
312+add_definitions(-DBOOST_THREAD_VERSION=4)
313+
314+add_library(local-provider-lib STATIC
315+ LocalDownloadJob.cpp
316+ LocalProvider.cpp
317+ LocalUploadJob.cpp
318+ utils.cpp
319+)
320+
321+target_include_directories(local-provider-lib PRIVATE
322+ ${Qt5Network_INCLUDE_DIRS}
323+ ${GLIB_DEPS_INCLUDE_DIRS}
324+ ${GIO_DEPS_INCLUDE_DIRS}
325+)
326+
327+set_target_properties(local-provider-lib PROPERTIES
328+ AUTOMOC TRUE
329+ POSITION_INDEPENDENT_CODE TRUE
330+)
331+
332+add_executable(storage-provider-local
333+ main.cpp
334+)
335+
336+target_link_libraries(storage-provider-local
337+ local-provider-lib
338+ storage-framework-provider
339+ Qt5::Network
340+ ${GLIB_DEPS_LIBRARIES}
341+ ${GIO_DEPS_LIBRARIES}
342+)
343+
344+install(
345+ TARGETS storage-provider-local
346+ RUNTIME DESTINATION ${CMAKE_INSTALL_LIBDIR}/${PROJECT_NAME}
347+)
348+
349+configure_file(com.canonical.StorageFramework.Provider.Local.service.in com.canonical.StorageFramework.Provider.Local.service)
350+
351+install(
352+ FILES ${CMAKE_CURRENT_BINARY_DIR}/com.canonical.StorageFramework.Provider.Local.service
353+ DESTINATION ${CMAKE_INSTALL_DATADIR}/dbus-1/services
354+)
355
356=== added file 'src/local-provider/LocalDownloadJob.cpp'
357--- src/local-provider/LocalDownloadJob.cpp 1970-01-01 00:00:00 +0000
358+++ src/local-provider/LocalDownloadJob.cpp 2017-03-17 05:04:57 +0000
359@@ -0,0 +1,179 @@
360+/*
361+ * Copyright (C) 2017 Canonical Ltd
362+ *
363+ * This program is free software: you can redistribute it and/or modify
364+ * it under the terms of the GNU Lesser General Public License version 3 as
365+ * published by the Free Software Foundation.
366+ *
367+ * This program is distributed in the hope that it will be useful,
368+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
369+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
370+ * GNU Lesser General Public License for more details.
371+ *
372+ * You should have received a copy of the GNU Lesser General Public License
373+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
374+ *
375+ * Authors: Michi Henning <michi.henning@canonical.com>
376+ */
377+
378+#include "LocalDownloadJob.h"
379+
380+#include "LocalProvider.h"
381+#include "utils.h"
382+#include <unity/storage/internal/safe_strerror.h>
383+#include <unity/storage/provider/Exceptions.h>
384+
385+using namespace unity::storage::provider;
386+using namespace std;
387+
388+static int next_download_id = 0;
389+
390+string const method = "download()";
391+
392+LocalDownloadJob::LocalDownloadJob(shared_ptr<LocalProvider> const& provider,
393+ string const& item_id,
394+ string const& match_etag)
395+ : DownloadJob(to_string(++next_download_id))
396+ , provider_(provider)
397+ , item_id_(item_id)
398+{
399+ using namespace boost::filesystem;
400+
401+ // Sanitize parameters.
402+ provider_->throw_if_not_valid(method, item_id_);
403+ try
404+ {
405+ auto st = status(item_id_);
406+ if (!is_regular_file(st))
407+ {
408+ throw InvalidArgumentException(method + ": \"" + item_id_ + "\" is not a file");
409+ }
410+ }
411+ // LCOV_EXCL_START // Too small a window to hit with a test.
412+ catch (filesystem_error const& e)
413+ {
414+ throw_storage_exception(method, e);
415+ }
416+ // LCOV_EXCL_STOP
417+ if (!match_etag.empty())
418+ {
419+ int64_t mtime = get_mtime_nsecs(method, item_id_);
420+ if (to_string(mtime) != match_etag)
421+ {
422+ throw ConflictException(method + ": etag mismatch");
423+ }
424+ }
425+
426+ // Make input file ready.
427+ QString filename = QString::fromStdString(item_id);
428+ file_.reset(new QFile(filename));
429+ if (!file_->open(QIODevice::ReadOnly))
430+ {
431+ throw_storage_exception(method,
432+ ": cannot open \"" + item_id + "\": " + file_->errorString().toStdString(),
433+ file_->error());
434+ }
435+ bytes_to_write_ = file_->size();
436+
437+ // Make write socket ready.
438+ int dup_fd = dup(write_socket());
439+ if (dup_fd == -1)
440+ {
441+ // LCOV_EXCL_START
442+ string msg = "LocalDownloadJob(): dup() failed: " + unity::storage::internal::safe_strerror(errno);
443+ throw ResourceException(msg, errno);
444+ // LCOV_EXCL_STOP
445+ }
446+ write_socket_.setSocketDescriptor(dup_fd, QLocalSocket::ConnectedState, QIODevice::WriteOnly);
447+ connect(&write_socket_, &QIODevice::bytesWritten, this, &LocalDownloadJob::on_bytes_written);
448+
449+ // Kick off the read-write cycle.
450+ QMetaObject::invokeMethod(this, "read_and_write_chunk", Qt::QueuedConnection);
451+}
452+
453+LocalDownloadJob::~LocalDownloadJob() = default;
454+
455+boost::future<void> LocalDownloadJob::cancel()
456+{
457+ disconnect(&write_socket_, nullptr, this, nullptr);
458+ write_socket_.abort();
459+ file_->close();
460+ return boost::make_ready_future();
461+}
462+
463+boost::future<void> LocalDownloadJob::finish()
464+{
465+ if (bytes_to_write_ > 0)
466+ {
467+ auto file_size = file_->size();
468+ auto written = file_size - bytes_to_write_;
469+ string msg = "finish() method called too early, file \"" + item_id_ + "\" has size "
470+ + to_string(file_size) + " but only " + to_string(written) + " bytes were consumed";
471+ cancel();
472+ return boost::make_exceptional_future<void>(LogicException(msg));
473+ }
474+ // LCOV_EXCL_START
475+ // Not reachable because we call report_complete() in read_and_write_chunk().
476+ return boost::make_ready_future();
477+ // LCOV_EXCL_STOP
478+}
479+
480+void LocalDownloadJob::on_bytes_written(qint64 bytes)
481+{
482+ bytes_to_write_ -= bytes;
483+ assert(bytes_to_write_ >= 0);
484+ read_and_write_chunk();
485+}
486+
487+void LocalDownloadJob::read_and_write_chunk()
488+{
489+ static qint64 constexpr READ_SIZE = 64 * 1024;
490+
491+ if (bytes_to_write_ == 0)
492+ {
493+ file_->close();
494+ write_socket_.close();
495+ report_complete();
496+ return;
497+ }
498+
499+ QByteArray buf;
500+ buf.resize(READ_SIZE);
501+ auto bytes_read = file_->read(buf.data(), buf.size());
502+ try
503+ {
504+ if (bytes_read == -1)
505+ {
506+ // LCOV_EXCL_START
507+ string msg = string("\"") + item_id_ + "\": read error: " + file_->errorString().toStdString();
508+ throw_storage_exception(method, msg, file_->error());
509+ // LCOV_EXCL_STOP
510+ }
511+ buf.resize(bytes_read);
512+
513+ auto bytes_written = write_socket_.write(buf);
514+ if (bytes_written == -1)
515+ {
516+ // LCOV_EXCL_START
517+ string msg = string("\"") + item_id_ + "\": socket error: " + write_socket_.errorString().toStdString();
518+ throw_storage_exception(method, msg, write_socket_.error());
519+ // LCOV_EXCL_STOP
520+ }
521+ else if (bytes_written != bytes_read)
522+ {
523+ // LCOV_EXCL_START
524+ string msg = string("\"") + item_id_ + "\": socket write error, requested " + to_string(bytes_read)
525+ + " B, but wrote only " + to_string(bytes_written) + " B.";
526+ throw_storage_exception(method, msg, QLocalSocket::UnknownSocketError);
527+ // LCOV_EXCL_STOP
528+ }
529+ }
530+ // LCOV_EXCL_START
531+ catch (std::exception const&)
532+ {
533+ write_socket_.abort();
534+ file_->close();
535+ report_error(current_exception());
536+ }
537+ // LCOV_EXCL_STOP
538+}
539
540=== added file 'src/local-provider/LocalDownloadJob.h'
541--- src/local-provider/LocalDownloadJob.h 1970-01-01 00:00:00 +0000
542+++ src/local-provider/LocalDownloadJob.h 2017-03-17 05:04:57 +0000
543@@ -0,0 +1,54 @@
544+/*
545+ * Copyright (C) 2017 Canonical Ltd
546+ *
547+ * This program is free software: you can redistribute it and/or modify
548+ * it under the terms of the GNU Lesser General Public License version 3 as
549+ * published by the Free Software Foundation.
550+ *
551+ * This program is distributed in the hope that it will be useful,
552+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
553+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
554+ * GNU Lesser General Public License for more details.
555+ *
556+ * You should have received a copy of the GNU Lesser General Public License
557+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
558+ *
559+ * Authors: Michi Henning <michi.henning@canonical.com>
560+ */
561+
562+#pragma once
563+
564+#include <unity/storage/provider/DownloadJob.h>
565+
566+#pragma GCC diagnostic push
567+#pragma GCC diagnostic ignored "-Wctor-dtor-privacy"
568+#pragma GCC diagnostic ignored "-Wswitch-default"
569+#include <QFile>
570+#include <QLocalSocket>
571+#pragma GCC diagnostic pop
572+
573+class LocalProvider;
574+
575+class LocalDownloadJob : public QObject, public unity::storage::provider::DownloadJob
576+{
577+ Q_OBJECT
578+public:
579+ LocalDownloadJob(std::shared_ptr<LocalProvider> const& provider,
580+ std::string const& item_id,
581+ std::string const& match_etag);
582+ virtual ~LocalDownloadJob();
583+
584+ virtual boost::future<void> cancel() override;
585+ virtual boost::future<void> finish() override;
586+
587+private Q_SLOTS:
588+ void on_bytes_written(qint64 bytes);
589+ void read_and_write_chunk();
590+
591+private:
592+ std::shared_ptr<LocalProvider> const provider_;
593+ std::string const item_id_;
594+ std::unique_ptr<QFile> file_;
595+ QLocalSocket write_socket_;
596+ int64_t bytes_to_write_;
597+};
598
599=== added file 'src/local-provider/LocalProvider.cpp'
600--- src/local-provider/LocalProvider.cpp 1970-01-01 00:00:00 +0000
601+++ src/local-provider/LocalProvider.cpp 2017-03-17 05:04:57 +0000
602@@ -0,0 +1,586 @@
603+/*
604+ * Copyright (C) 2017 Canonical Ltd
605+ *
606+ * This program is free software: you can redistribute it and/or modify
607+ * it under the terms of the GNU Lesser General Public License version 3 as
608+ * published by the Free Software Foundation.
609+ *
610+ * This program is distributed in the hope that it will be useful,
611+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
612+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
613+ * GNU Lesser General Public License for more details.
614+ *
615+ * You should have received a copy of the GNU Lesser General Public License
616+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
617+ *
618+ * Authors: Michi Henning <michi.henning@canonical.com>
619+ */
620+
621+#include "LocalProvider.h"
622+
623+#include "LocalDownloadJob.h"
624+#include "LocalUploadJob.h"
625+#include "utils.h"
626+
627+#include <unity/storage/internal/gobj_memory.h>
628+#include <unity/storage/provider/Exceptions.h>
629+
630+#include <boost/algorithm/string.hpp>
631+
632+#pragma GCC diagnostic push
633+#pragma GCC diagnostic ignored "-Wold-style-cast"
634+#include <gio/gio.h>
635+#include <glib.h>
636+#pragma GCC diagnostic pop
637+
638+using namespace unity::storage::provider;
639+using namespace std;
640+
641+namespace
642+{
643+
644+// Return the root directory where we store files.
645+// If SF_LOCAL_PROVIDER_ROOT is set (used for testing), any files are created
646+// directly under the root. E.g., if we do root.createFile("foo.txt", ...), the file
647+// will be created as ${SF_LOCAL_PROVIDER_ROOT/foo.txt. SF_LOCAL_PROVIDER_ROOT must
648+// be a pre-existing directory.
649+//
650+// Otherwise, the root is determined by SNAP_USER_COMMON or, if that is not set,
651+// by XDG_DATA_HOME. Either way, files are created in a storage-framework/local
652+// subdirectory. E.g., if SNAP_USER_COMMON or XDG_DATA_HOME is set to "/tmp" and
653+// we do root.createFile("foo.txt", ...), the file will be created as
654+// /tmp/storage-framework/local/foo.txt. If /tmp/storage-framework/local does not exist,
655+// the directory will be created.
656+
657+string get_root_dir(string const& method)
658+{
659+ using namespace boost::filesystem;
660+
661+ char const* dir = getenv("SF_LOCAL_PROVIDER_ROOT");
662+ if (dir && *dir != '\0')
663+ {
664+ boost::system::error_code ec;
665+ if (!exists(dir, ec) || !is_directory(dir, ec))
666+ {
667+ string msg = method + ": Environment variable SF_LOCAL_PROVIDER_ROOT must denote an existing directory";
668+ throw InvalidArgumentException(msg);
669+ }
670+ return dir;
671+ }
672+
673+ string data_dir;
674+ dir = getenv("SNAP_USER_COMMON");
675+ if (dir && *dir != '\0')
676+ {
677+ data_dir = dir;
678+ }
679+ else
680+ {
681+ data_dir = g_get_user_data_dir(); // Never fails.
682+ }
683+ data_dir += "/storage-framework/local";
684+
685+ try
686+ {
687+ create_directories(data_dir);
688+ }
689+ catch (filesystem_error const& e)
690+ {
691+ throw_storage_exception(method, e);
692+ }
693+
694+ return data_dir;
695+}
696+
697+// Copy a file or directory (recursively). Ignore anything that has the temp file prefix
698+// or is not a file or directory.
699+
700+void copy_recursively(boost::filesystem::path const& source, boost::filesystem::path const& target)
701+{
702+ using namespace boost::filesystem;
703+
704+ if (is_reserved_path(source))
705+ {
706+ return; // Don't copy temporary directories.
707+ }
708+
709+ auto s = status(source);
710+ if (is_regular_file(s))
711+ {
712+ copy_file(source, target);
713+ }
714+ else if (is_directory(s))
715+ {
716+ copy_directory(source, target); // Poorly named in boost; this creates the target dir without recursion
717+ for (directory_iterator it(source); it != directory_iterator(); ++it)
718+ {
719+ path source_entry = it->path();
720+ path target_entry = target;
721+ target_entry /= source_entry.filename();
722+ copy_recursively(source_entry, target_entry);
723+ }
724+ }
725+ else
726+ {
727+ // Ignore everything that's not a directory or file.
728+ }
729+}
730+
731+// Convert nanoseconds since the epoch into ISO 8601 date-time.
732+
733+string make_iso_date(int64_t nsecs_since_epoch)
734+{
735+ static char const* const FMT = "%Y-%m-%dT%TZ"; // ISO 8601, no fractional seconds.
736+
737+ struct tm time;
738+ time_t secs_since_epoch = nsecs_since_epoch / 1000000000;
739+ gmtime_r(&secs_since_epoch, &time);
740+
741+ char buf[128];
742+ strftime(buf, sizeof(buf), FMT, &time);
743+ return buf;
744+}
745+
746+string get_content_type(string const& filename)
747+{
748+ using namespace unity::storage::internal;
749+
750+ static string const unknown_content_type = "application/octet-stream";
751+
752+ gobj_ptr<GFile> file(g_file_new_for_path(filename.c_str()));
753+ assert(file); // Cannot fail according to doc.
754+
755+ GError* err = nullptr;
756+ gobj_ptr<GFileInfo> full_info(g_file_query_info(file.get(),
757+ G_FILE_ATTRIBUTE_STANDARD_FAST_CONTENT_TYPE,
758+ G_FILE_QUERY_INFO_NONE,
759+ /* cancellable */ NULL,
760+ &err));
761+ if (!full_info)
762+ {
763+ return unknown_content_type; // LCOV_EXCL_LINE
764+ }
765+
766+ string content_type = g_file_info_get_attribute_string(full_info.get(), G_FILE_ATTRIBUTE_STANDARD_FAST_CONTENT_TYPE);
767+ if (content_type.empty())
768+ {
769+ return unknown_content_type; // LCOV_EXCL_LINE
770+ }
771+ return content_type;
772+}
773+
774+// Simple wrapper template that deals with exception handling so we don't
775+// have to repeat ourselves endlessly in the various lambdas below.
776+// The auto deduction of the return type requires C++ 14.
777+
778+template<typename F>
779+auto invoke_async(string const& method, F& functor)
780+{
781+ auto lambda = [method, functor]
782+ {
783+ try
784+ {
785+ return functor();
786+ }
787+ catch (StorageException const&)
788+ {
789+ throw;
790+ }
791+ catch (boost::filesystem::filesystem_error const& e)
792+ {
793+ throw_storage_exception(method, e);
794+ }
795+ // LCOV_EXCL_START
796+ catch (std::exception const& e)
797+ {
798+ throw boost::enable_current_exception(UnknownException(e.what()));
799+ }
800+ // LCOV_EXCL_STOP
801+ };
802+ // TODO: boost::async is potentially expensive for some operations. Consider boost::asio thread pool?
803+ return boost::async(boost::launch::async, lambda);
804+}
805+
806+} // namespace
807+
808+LocalProvider::LocalProvider()
809+ : root_(boost::filesystem::canonical(get_root_dir("LocalProvider()")))
810+{
811+}
812+
813+LocalProvider::~LocalProvider() = default;
814+
815+boost::future<ItemList> LocalProvider::roots(vector<string> const& /* keys */, Context const& /* context */)
816+{
817+ vector<Item> roots{ make_item("roots()", root_, status(root_)) };
818+ return boost::make_ready_future(roots);
819+}
820+
821+boost::future<tuple<ItemList, string>> LocalProvider::list(string const& item_id,
822+ string const& page_token,
823+ vector<string> const& /* keys */,
824+ Context const& /* context */)
825+{
826+ string const method = "list()";
827+
828+ auto This = dynamic_pointer_cast<LocalProvider>(shared_from_this());
829+ auto do_list = [This, method, item_id, page_token]
830+ {
831+ using namespace boost::filesystem;
832+
833+ This->throw_if_not_valid(method, item_id);
834+ vector<Item> items;
835+ for (directory_iterator it(item_id); it != directory_iterator(); ++it)
836+ {
837+ auto dirent = *it;
838+ auto path = dirent.path();
839+ if (is_reserved_path(path))
840+ {
841+ continue; // Hide temp files that we create during copy() and move().
842+ }
843+ Item i;
844+ try
845+ {
846+ auto st = dirent.status();
847+ i = This->make_item(method, path, st);
848+ items.push_back(i);
849+ }
850+ catch (std::exception const&)
851+ {
852+ // We ignore weird errors (such as entries that are not files or folders).
853+ }
854+ }
855+ return tuple<ItemList, string>(items, "");
856+ };
857+
858+ return invoke_async(method, do_list);
859+}
860+
861+boost::future<ItemList> LocalProvider::lookup(string const& parent_id,
862+ string const& name,
863+ vector<string> const& /* keys */,
864+ Context const& /* context */)
865+{
866+ string const method = "lookup()";
867+
868+ auto This = dynamic_pointer_cast<LocalProvider>(shared_from_this());
869+ auto do_lookup = [This, method, parent_id, name]
870+ {
871+ using namespace boost::filesystem;
872+
873+ This->throw_if_not_valid(method, parent_id);
874+ auto sanitized_name = sanitize(method, name);
875+ path p = parent_id;
876+ p /= sanitized_name;
877+ This->throw_if_not_valid(method, p.native());
878+ auto st = status(p);
879+ return vector<Item>{ This->make_item(method, p, st) };
880+ };
881+
882+ return invoke_async(method, do_lookup);
883+}
884+
885+boost::future<Item> LocalProvider::metadata(string const& item_id,
886+ vector<string> const& /* keys */,
887+ Context const& /* context */)
888+{
889+ string const method = "metadata()";
890+
891+ auto This = dynamic_pointer_cast<LocalProvider>(shared_from_this());
892+ auto do_metadata = [This, method, item_id]
893+ {
894+ using namespace boost::filesystem;
895+
896+ This->throw_if_not_valid(method, item_id);
897+ path p = item_id;
898+ auto st = status(p);
899+ return This->make_item(method, p, st);
900+ };
901+
902+ return invoke_async(method, do_metadata);
903+}
904+
905+boost::future<Item> LocalProvider::create_folder(string const& parent_id,
906+ string const& name,
907+ vector<string> const& /* keys */,
908+ Context const& /* context */)
909+{
910+ string const method = "create_folder()";
911+
912+ auto This = dynamic_pointer_cast<LocalProvider>(shared_from_this());
913+ auto do_create = [This, method, parent_id, name]
914+ {
915+ using namespace boost::filesystem;
916+
917+ This->throw_if_not_valid(method, parent_id);
918+ auto sanitized_name = sanitize(method, name);
919+ path p = parent_id;
920+ p /= sanitized_name;
921+ // create_directory() succeeds if the directory exists already, so we need to check explicitly.
922+ if (exists(p))
923+ {
924+ string msg = method + ": \"" + p.native() + "\" exists already";
925+ throw boost::enable_current_exception(ExistsException(msg, p.native(), name));
926+ }
927+ create_directory(p);
928+ auto st = status(p);
929+ return This->make_item(method, p, st);
930+ };
931+
932+ return invoke_async(method, do_create);
933+}
934+
935+boost::future<unique_ptr<UploadJob>> LocalProvider::create_file(string const& parent_id,
936+ string const& name,
937+ int64_t size,
938+ string const& /* content_type */,
939+ bool allow_overwrite,
940+ vector<string> const& /* keys */,
941+ Context const& /* context */)
942+{
943+ auto This = dynamic_pointer_cast<LocalProvider>(shared_from_this());
944+ boost::promise<unique_ptr<UploadJob>> p;
945+ p.set_value(make_unique<LocalUploadJob>(This, parent_id, name, size, allow_overwrite));
946+ return p.get_future();
947+}
948+
949+boost::future<unique_ptr<UploadJob>> LocalProvider::update(string const& item_id,
950+ int64_t size,
951+ string const& old_etag,
952+ vector<string> const& /* keys */,
953+ Context const& /* context */)
954+{
955+ auto This = dynamic_pointer_cast<LocalProvider>(shared_from_this());
956+ boost::promise<unique_ptr<UploadJob>> p;
957+ p.set_value(make_unique<LocalUploadJob>(This, item_id, size, old_etag));
958+ return p.get_future();
959+}
960+
961+boost::future<unique_ptr<DownloadJob>> LocalProvider::download(string const& item_id,
962+ string const& match_etag,
963+ Context const& /* context */)
964+{
965+ auto This = dynamic_pointer_cast<LocalProvider>(shared_from_this());
966+ boost::promise<unique_ptr<DownloadJob>> p;
967+ p.set_value(make_unique<LocalDownloadJob>(This, item_id, match_etag));
968+ return p.get_future();
969+}
970+
971+boost::future<void> LocalProvider::delete_item(string const& item_id, Context const& /* context */)
972+{
973+ string const method = "delete_item()";
974+
975+ auto This = dynamic_pointer_cast<LocalProvider>(shared_from_this());
976+ auto do_delete = [This, method, item_id]
977+ {
978+ using namespace boost::filesystem;
979+
980+ This->throw_if_not_valid(method, item_id);
981+ if (canonical(item_id).native() == This->root_)
982+ {
983+ string msg = method + ": cannot delete root";
984+ throw boost::enable_current_exception(LogicException(msg));
985+ }
986+ remove_all(item_id);
987+ };
988+
989+ return invoke_async(method, do_delete);
990+}
991+
992+boost::future<Item> LocalProvider::move(string const& item_id,
993+ string const& new_parent_id,
994+ string const& new_name,
995+ vector<string> const& /* keys */,
996+ Context const& /* context */)
997+{
998+ string const method = "move()";
999+
1000+ auto This = dynamic_pointer_cast<LocalProvider>(shared_from_this());
1001+ auto do_move = [This, method, item_id, new_parent_id, new_name]
1002+ {
1003+ using namespace boost::filesystem;
1004+
1005+ This->throw_if_not_valid(method, item_id);
1006+ This->throw_if_not_valid(method, new_parent_id);
1007+ auto sanitized_name = sanitize(method, new_name);
1008+
1009+ path parent_path = new_parent_id;
1010+ path target_path = parent_path / sanitized_name;
1011+
1012+ if (exists(target_path))
1013+ {
1014+ string msg = method + ": \"" + target_path.native() + "\" exists already";
1015+ throw boost::enable_current_exception(ExistsException(msg, target_path.native(), new_name));
1016+ }
1017+
1018+ // Small race condition here: if exists() just said that the target does not exist, it is
1019+ // possible for it to have been created since. If so, if the target is a file or an empty
1020+ // directory, it will be removed. In practice, this is unlikely to happen and, if it does,
1021+ // it is not the end of the world.
1022+ // TODO: deal with EXDEV
1023+ rename(item_id, target_path);
1024+ auto st = status(target_path);
1025+ return This->make_item(method, target_path, st);
1026+ };
1027+
1028+ return invoke_async(method, do_move);
1029+}
1030+
1031+boost::future<Item> LocalProvider::copy(string const& item_id,
1032+ string const& new_parent_id,
1033+ string const& new_name,
1034+ vector<string> const& /* keys */,
1035+ Context const& /* context */)
1036+{
1037+ string const method = "copy()";
1038+
1039+ auto This = dynamic_pointer_cast<LocalProvider>(shared_from_this());
1040+ auto do_copy = [This, method, item_id, new_parent_id, new_name]
1041+ {
1042+ using namespace boost::filesystem;
1043+
1044+ This->throw_if_not_valid(method, item_id);
1045+ This->throw_if_not_valid(method, new_parent_id);
1046+ auto sanitized_name = sanitize(method, new_name);
1047+
1048+ path parent_path = new_parent_id;
1049+ path target_path = parent_path / sanitized_name;
1050+
1051+ if (is_directory(item_id))
1052+ {
1053+ if (exists(target_path))
1054+ {
1055+ string msg = method + ": \"" + target_path.native() + "\" exists already";
1056+ throw boost::enable_current_exception(ExistsException(msg, target_path.native(), new_name));
1057+ }
1058+
1059+ // For recursive copy, we create a temporary directory in lieu of target_path and recursively copy
1060+ // everything into the temporary directory. This ensures that we don't invalidate directory iterators
1061+ // by creating things while we are iterating, potentially getting trapped in an infinite loop.
1062+ path tmp_path = canonical(parent_path);
1063+ tmp_path /= unique_path(string(TMPFILE_PREFIX) + "-%%%%-%%%%-%%%%-%%%%");
1064+ create_directories(tmp_path);
1065+ for (directory_iterator it(item_id); it != directory_iterator(); ++it)
1066+ {
1067+ if (is_reserved_path(it->path()))
1068+ {
1069+ continue; // Don't recurse into the temporary directory
1070+ }
1071+ file_status s = it->status();
1072+ if (is_directory(s) || is_regular_file(s))
1073+ {
1074+ path source_entry = it->path();
1075+ path target_entry = tmp_path;
1076+ target_entry /= source_entry.filename();
1077+ copy_recursively(source_entry, target_entry);
1078+ }
1079+ }
1080+ rename(tmp_path, target_path);
1081+ }
1082+ else
1083+ {
1084+ copy_file(item_id, target_path);
1085+ }
1086+
1087+ auto st = status(target_path);
1088+ return This->make_item(method, target_path, st);
1089+ };
1090+
1091+ return invoke_async(method, do_copy);
1092+}
1093+
1094+// Make sure that id does not point outside the root.
1095+
1096+void LocalProvider::throw_if_not_valid(string const& method, string const& id) const
1097+{
1098+ using namespace boost::filesystem;
1099+
1100+ string suspect_id;
1101+ try
1102+ {
1103+ suspect_id = canonical(id).native();
1104+ }
1105+ catch (filesystem_error const& e)
1106+ {
1107+ throw_storage_exception(method, e);
1108+ }
1109+
1110+ // Disallow things such as <root>/blah/../blah even though they lead to the correct path.
1111+ if (suspect_id != id)
1112+ {
1113+ throw boost::enable_current_exception(InvalidArgumentException(method + ": invalid id: \"" + id + "\""));
1114+ }
1115+ // id must denote the root or have the root as a prefix.
1116+ auto const root_id = root_.native();
1117+ if (id != root_id && !boost::starts_with(id, root_id + "/"))
1118+ {
1119+ throw boost::enable_current_exception(InvalidArgumentException(method + ": invalid id: \"" + id + "\""));
1120+ }
1121+}
1122+
1123+// Return an Item initialized from item_path and st.
1124+
1125+Item LocalProvider::make_item(string const& method,
1126+ boost::filesystem::path const& item_path,
1127+ boost::filesystem::file_status const& st) const
1128+{
1129+ using namespace unity::storage;
1130+ using namespace unity::storage::metadata;
1131+ using namespace boost::filesystem;
1132+
1133+ map<string, MetadataValue> meta;
1134+
1135+ string const item_id = item_path.native();
1136+ int64_t const mtime_nsecs = get_mtime_nsecs(method, item_id);
1137+ string const iso_mtime = make_iso_date(mtime_nsecs);
1138+
1139+ ItemType type;
1140+ string name = item_path.filename().native();
1141+ vector<string> parents{item_path.parent_path().native()};
1142+ string etag;
1143+ switch (st.type())
1144+ {
1145+ case regular_file:
1146+ type = ItemType::file;
1147+ etag = to_string(mtime_nsecs);
1148+ meta.insert({SIZE_IN_BYTES, int64_t(file_size(item_path))});
1149+ break;
1150+ case directory_file:
1151+ if (item_path == root_)
1152+ {
1153+ name = "/";
1154+ parents.clear();
1155+ type = ItemType::root;
1156+ }
1157+ else
1158+ {
1159+ type = ItemType::folder;
1160+ }
1161+ break;
1162+ default:
1163+ throw boost::enable_current_exception(
1164+ NotExistsException(method + ": \"" + item_id + "\" is neither a file nor a folder", item_id));
1165+ }
1166+
1167+
1168+ auto const info = space(item_path);
1169+ meta.insert({FREE_SPACE_BYTES, int64_t(info.available)});
1170+ meta.insert({USED_SPACE_BYTES, int64_t(info.capacity - info.available)});
1171+
1172+ meta.insert({LAST_MODIFIED_TIME, iso_mtime});
1173+ meta.insert({CONTENT_TYPE, get_content_type(item_id)});
1174+
1175+ auto perms = st.permissions();
1176+ bool writable;
1177+ if (type == ItemType::file)
1178+ {
1179+ writable = perms & owner_write;
1180+ }
1181+ else
1182+ {
1183+ writable = perms & owner_write && perms & owner_exe;
1184+ }
1185+ meta.insert({WRITABLE, writable});
1186+
1187+ return Item{ item_id, parents, name, etag, type, meta };
1188+}
1189
1190=== added file 'src/local-provider/LocalProvider.h'
1191--- src/local-provider/LocalProvider.h 1970-01-01 00:00:00 +0000
1192+++ src/local-provider/LocalProvider.h 2017-03-17 05:04:57 +0000
1193@@ -0,0 +1,84 @@
1194+/*
1195+ * Copyright (C) 2017 Canonical Ltd
1196+ *
1197+ * This program is free software: you can redistribute it and/or modify
1198+ * it under the terms of the GNU Lesser General Public License version 3 as
1199+ * published by the Free Software Foundation.
1200+ *
1201+ * This program is distributed in the hope that it will be useful,
1202+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1203+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1204+ * GNU Lesser General Public License for more details.
1205+ *
1206+ * You should have received a copy of the GNU Lesser General Public License
1207+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1208+ *
1209+ * Authors: Michi Henning <michi.henning@canonical.com>
1210+ */
1211+
1212+#pragma once
1213+
1214+#include <unity/storage/provider/ProviderBase.h>
1215+
1216+#include <boost/filesystem.hpp>
1217+
1218+class LocalProvider : public unity::storage::provider::ProviderBase
1219+{
1220+public:
1221+ LocalProvider();
1222+ virtual ~LocalProvider();
1223+
1224+ boost::future<unity::storage::provider::ItemList> roots(
1225+ std::vector<std::string> const& metadata_keys,
1226+ unity::storage::provider::Context const& ctx) override;
1227+ boost::future<std::tuple<unity::storage::provider::ItemList, std::string>> list(
1228+ std::string const& item_id, std::string const& page_token,
1229+ std::vector<std::string> const& metadata_keys,
1230+ unity::storage::provider::Context const& ctx) override;
1231+ boost::future<unity::storage::provider::ItemList> lookup(
1232+ std::string const& parent_id, std::string const& name,
1233+ std::vector<std::string> const& metadata_keys,
1234+ unity::storage::provider::Context const& ctx) override;
1235+ boost::future<unity::storage::provider::Item> metadata(
1236+ std::string const& item_id,
1237+ std::vector<std::string> const& metadata_keys,
1238+ unity::storage::provider::Context const& ctx) override;
1239+ boost::future<unity::storage::provider::Item> create_folder(
1240+ std::string const& parent_id, std::string const& name,
1241+ std::vector<std::string> const& metadata_keys,
1242+ unity::storage::provider::Context const& ctx) override;
1243+ boost::future<std::unique_ptr<unity::storage::provider::UploadJob>> create_file(
1244+ std::string const& parent_id, std::string const& name,
1245+ int64_t size, std::string const& content_type, bool allow_overwrite,
1246+ std::vector<std::string> const& metadata_keys,
1247+ unity::storage::provider::Context const& ctx) override;
1248+ boost::future<std::unique_ptr<unity::storage::provider::UploadJob>> update(
1249+ std::string const& item_id, int64_t size,
1250+ std::string const& old_etag,
1251+ std::vector<std::string> const& metadata_keys,
1252+ unity::storage::provider::Context const& ctx) override;
1253+ boost::future<std::unique_ptr<unity::storage::provider::DownloadJob>> download(
1254+ std::string const& item_id,
1255+ std::string const& match_etag,
1256+ unity::storage::provider::Context const& ctx) override;
1257+ boost::future<void> delete_item(std::string const& item_id,
1258+ unity::storage::provider::Context const& ctx) override;
1259+ boost::future<unity::storage::provider::Item> move(
1260+ std::string const& item_id, std::string const& new_parent_id,
1261+ std::string const& new_name,
1262+ std::vector<std::string> const& metadata_keys,
1263+ unity::storage::provider::Context const& ctx) override;
1264+ boost::future<unity::storage::provider::Item> copy(
1265+ std::string const& item_id, std::string const& new_parent_id,
1266+ std::string const& new_name,
1267+ std::vector<std::string> const& metadata_keys,
1268+ unity::storage::provider::Context const& ctx) override;
1269+
1270+ void throw_if_not_valid(std::string const& method, std::string const& id) const;
1271+ unity::storage::provider::Item make_item(std::string const& method,
1272+ boost::filesystem::path const& item_path,
1273+ boost::filesystem::file_status const& st) const;
1274+
1275+private:
1276+ boost::filesystem::path const root_;
1277+};
1278
1279=== added file 'src/local-provider/LocalUploadJob.cpp'
1280--- src/local-provider/LocalUploadJob.cpp 1970-01-01 00:00:00 +0000
1281+++ src/local-provider/LocalUploadJob.cpp 2017-03-17 05:04:57 +0000
1282@@ -0,0 +1,338 @@
1283+/*
1284+ * Copyright (C) 2017 Canonical Ltd
1285+ *
1286+ * This program is free software: you can redistribute it and/or modify
1287+ * it under the terms of the GNU Lesser General Public License version 3 as
1288+ * published by the Free Software Foundation.
1289+ *
1290+ * This program is distributed in the hope that it will be useful,
1291+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1292+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1293+ * GNU Lesser General Public License for more details.
1294+ *
1295+ * You should have received a copy of the GNU Lesser General Public License
1296+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1297+ *
1298+ * Authors: Michi Henning <michi.henning@canonical.com>
1299+ */
1300+
1301+#include "LocalUploadJob.h"
1302+
1303+#include "LocalProvider.h"
1304+#include "utils.h"
1305+#include <unity/storage/internal/safe_strerror.h>
1306+#include <unity/storage/provider/Exceptions.h>
1307+#include <unity/storage/provider/ProviderBase.h> // TODO: Should not be needed
1308+
1309+#include <fcntl.h>
1310+
1311+using namespace unity::storage::provider;
1312+using namespace std;
1313+
1314+static int next_upload_id = 0;
1315+
1316+LocalUploadJob::LocalUploadJob(shared_ptr<LocalProvider> const& provider, int64_t size, const string& method)
1317+ : UploadJob(to_string(++next_upload_id))
1318+ , provider_(provider)
1319+ , size_(size)
1320+ , bytes_to_write_(size)
1321+ , method_(method)
1322+ , state_(in_progress)
1323+ , tmp_fd_([](int fd){ if (fd != -1) ::close(fd); })
1324+{
1325+}
1326+
1327+LocalUploadJob::LocalUploadJob(shared_ptr<LocalProvider> const& provider,
1328+ string const& parent_id,
1329+ string const& name,
1330+ int64_t size,
1331+ bool allow_overwrite)
1332+ : LocalUploadJob(provider, size, "create_file()")
1333+{
1334+ using namespace boost::filesystem;
1335+
1336+ parent_id_ = parent_id;
1337+ allow_overwrite_ = allow_overwrite;
1338+
1339+ provider_->throw_if_not_valid(method_, parent_id);
1340+
1341+ auto sanitized_name = sanitize(method_, name);
1342+ path p = parent_id;
1343+ p /= sanitized_name;
1344+ item_id_ = p.native();
1345+ if (!allow_overwrite && exists(item_id_))
1346+ {
1347+ string msg = method_ + ": \"" + item_id_ + "\" exists already";
1348+ throw ExistsException(msg, item_id_, sanitized_name.native());
1349+ }
1350+
1351+ prepare_channels();
1352+}
1353+
1354+LocalUploadJob::LocalUploadJob(shared_ptr<LocalProvider> const& provider,
1355+ string const& item_id,
1356+ int64_t size,
1357+ string const& old_etag)
1358+ : LocalUploadJob(provider, size, "update()")
1359+{
1360+ using namespace boost::filesystem;
1361+
1362+ item_id_ = item_id;
1363+ provider_->throw_if_not_valid(method_, item_id);
1364+ try
1365+ {
1366+ auto st = status(item_id);
1367+ if (!is_regular_file(st))
1368+ {
1369+ throw InvalidArgumentException(method_ + ": \"" + item_id + "\" is not a file");
1370+ }
1371+ }
1372+ // LCOV_EXCL_START
1373+ catch (filesystem_error const& e)
1374+ {
1375+ // The call to status could throw if the file is unlinked immediately
1376+ // after the call to throw_if_not_valid.
1377+ throw_storage_exception(method_, e);
1378+ }
1379+ // LCOV_EXCL_STOP
1380+ if (!old_etag.empty())
1381+ {
1382+ int64_t mtime = get_mtime_nsecs(method_, item_id);
1383+ if (to_string(mtime) != old_etag)
1384+ {
1385+ throw ConflictException(method_ + ": etag mismatch");
1386+ }
1387+ }
1388+ old_etag_ = old_etag;
1389+
1390+ prepare_channels();
1391+}
1392+
1393+LocalUploadJob::~LocalUploadJob() = default;
1394+
1395+void LocalUploadJob::prepare_channels()
1396+{
1397+ using namespace boost::filesystem;
1398+
1399+ // Open tmp file for writing.
1400+ auto parent_path = path(item_id_).parent_path();
1401+ tmp_fd_.reset(open(parent_path.native().c_str(), O_TMPFILE | O_WRONLY, 0600));
1402+ if (tmp_fd_.get() == -1)
1403+ {
1404+ // Some kernels on the phones don't support O_TMPFILE and return various errno values when this fails.
1405+ // So, if anything at all goes wrong, we fall back on conventional temp file creation and
1406+ // produce a hard error if that doesn't work either.
1407+ // Note that, in this case, the temp file retains its name in the file system. Not nice because,
1408+ // if this process dies at the wrong moment, we leave the temp file behind.
1409+ use_linkat_ = false;
1410+ string tmpfile = parent_path.native() + "/" + TMPFILE_PREFIX + "-%%%%-%%%%-%%%%-%%%%";
1411+ tmp_fd_.reset(mkstemp(const_cast<char*>(tmpfile.data())));
1412+ if (tmp_fd_.get() == -1)
1413+ {
1414+ string msg = method_ + ": cannot create temp file \"" + tmpfile + "\": "
1415+ + unity::storage::internal::safe_strerror(errno);
1416+ throw ResourceException(msg, errno);
1417+ }
1418+ // LCOV_EXCL_START
1419+ file_.reset(new QFile(QString::fromStdString(tmpfile)));
1420+ file_->open(QIODevice::WriteOnly);
1421+ // LCOV_EXCL_STOP
1422+ }
1423+ else
1424+ {
1425+ use_linkat_ = true;
1426+ file_.reset(new QFile);
1427+ file_->open(tmp_fd_.get(), QIODevice::WriteOnly, QFileDevice::DontCloseHandle);
1428+ }
1429+
1430+ // Make read socket ready.
1431+ int dup_fd = dup(read_socket());
1432+ if (dup_fd == -1)
1433+ {
1434+ // LCOV_EXCL_START
1435+ string msg = method_ + ": dup() failed: " + unity::storage::internal::safe_strerror(errno);
1436+ throw ResourceException(msg, errno);
1437+ // LCOV_EXCL_STOP
1438+ }
1439+ read_socket_.setSocketDescriptor(dup_fd, QLocalSocket::ConnectedState, QIODevice::ReadOnly);
1440+ connect(&read_socket_, &QLocalSocket::readyRead, this, &LocalUploadJob::on_bytes_ready);
1441+ connect(&read_socket_, &QIODevice::readChannelFinished, this, &LocalUploadJob::on_read_channel_finished);
1442+}
1443+
1444+boost::future<void> LocalUploadJob::cancel()
1445+{
1446+ if (state_ == in_progress)
1447+ {
1448+ abort_upload();
1449+ }
1450+ return boost::make_ready_future();
1451+}
1452+
1453+boost::future<Item> LocalUploadJob::finish()
1454+{
1455+ on_bytes_ready(); // Read any remaining unread buffered data.
1456+
1457+ if (bytes_to_write_ > 0)
1458+ {
1459+ string msg = "finish() method called too early, size was given as "
1460+ + to_string(size_) + " but only "
1461+ + to_string(size_ - bytes_to_write_) + " bytes were received";
1462+ return boost::make_exceptional_future<Item>(LogicException(msg));
1463+ }
1464+
1465+ // We are committed to finishing successfully or with an error now.
1466+ state_ = finished;
1467+
1468+ try
1469+ {
1470+ // We check again for an etag mismatch or overwrite, in case the file was updated after the upload started.
1471+ if (!parent_id_.empty())
1472+ {
1473+ // create_file()
1474+ if (!allow_overwrite_ && boost::filesystem::exists(item_id_))
1475+ {
1476+ string msg = method_ + ": \"" + item_id_ + "\" exists already";
1477+ boost::filesystem::path(item_id_).filename().native();
1478+ BOOST_THROW_EXCEPTION(
1479+ ExistsException(msg, item_id_, boost::filesystem::path(item_id_).filename().native()));
1480+ }
1481+ }
1482+ else if (!old_etag_.empty())
1483+ {
1484+ // update()
1485+ int64_t mtime = get_mtime_nsecs(method_, item_id_);
1486+ if (to_string(mtime) != old_etag_)
1487+ {
1488+ BOOST_THROW_EXCEPTION(ConflictException(method_ + ": etag mismatch"));
1489+ }
1490+ }
1491+
1492+ if (!file_->flush()) // Make sure that all buffered data is written.
1493+ {
1494+ // LCOV_EXCL_START
1495+ string msg = "finish(): cannot flush output file: " + file_->errorString().toStdString();
1496+ throw_storage_exception("finish()", msg, file_->error());
1497+ // LCOV_EXCL_STOP
1498+ }
1499+
1500+ // Link the anonymous tmp file into the file system.
1501+ using namespace unity::storage::internal;
1502+
1503+ if (use_linkat_)
1504+ {
1505+ auto old_path = string("/proc/self/fd/") + std::to_string(tmp_fd_.get());
1506+ ::unlink(item_id_.c_str()); // linkat() will not remove existing file: http://lwn.net/Articles/559969/
1507+ if (linkat(-1, old_path.c_str(), tmp_fd_.get(), item_id_.c_str(), AT_SYMLINK_FOLLOW) == -1)
1508+ {
1509+ // LCOV_EXCL_START
1510+ string msg = "finish(): linkat \"" + old_path + "\" to \"" + item_id_ + "\" failed: "
1511+ + safe_strerror(errno);
1512+ BOOST_THROW_EXCEPTION(ResourceException(msg, errno));
1513+ // LCOV_EXCL_STOP
1514+ }
1515+ }
1516+ else
1517+ {
1518+ // LCOV_EXCL_START
1519+ auto old_path = file_->fileName().toStdString();
1520+ if (rename(old_path.c_str(), item_id_.c_str()) == -1)
1521+ {
1522+ string msg = "finish(): rename \"" + old_path + "\" to \"" + item_id_ + "\" failed: "
1523+ + safe_strerror(errno);
1524+ BOOST_THROW_EXCEPTION(ResourceException(msg, errno));
1525+ }
1526+ // LCOV_EXCL_STOP
1527+ }
1528+
1529+ file_->close();
1530+ read_socket_.close();
1531+
1532+ auto st = boost::filesystem::status(item_id_);
1533+ return boost::make_ready_future<Item>(provider_->make_item(method_, item_id_, st));
1534+ }
1535+ catch (StorageException const&)
1536+ {
1537+ return boost::make_exceptional_future<Item>(boost::current_exception());
1538+ }
1539+ // LCOV_EXCL_START
1540+ catch (boost::filesystem::filesystem_error const& e)
1541+ {
1542+ try
1543+ {
1544+ throw_storage_exception("finish()", e);
1545+ }
1546+ catch (StorageException const&)
1547+ {
1548+ return boost::make_exceptional_future<Item>(boost::current_exception());
1549+ }
1550+ }
1551+ catch (std::exception const& e)
1552+ {
1553+ return boost::make_exceptional_future<Item>(UnknownException(e.what()));
1554+ }
1555+ // LCOV_EXCL_STOP
1556+}
1557+
1558+void LocalUploadJob::on_bytes_ready()
1559+{
1560+ if (bytes_to_write_ < 0)
1561+ {
1562+ return; // LCOV_EXCL_LINE // We received too many bytes earlier.
1563+ }
1564+
1565+ try
1566+ {
1567+ auto buf = read_socket_.readAll();
1568+ if (buf.size() != 0)
1569+ {
1570+ bytes_to_write_ -= buf.size();
1571+ if (bytes_to_write_ < 0)
1572+ {
1573+ string msg = method_ + ": received more than the expected number (" + to_string(size_) + ") of bytes";
1574+ throw LogicException(msg);
1575+ }
1576+ auto bytes_written = file_->write(buf);
1577+ if (bytes_written == -1)
1578+ {
1579+ // LCOV_EXCL_START
1580+ string msg = "write error: " + file_->errorString().toStdString();
1581+ throw_storage_exception(method_, msg, file_->error());
1582+ // LCOV_EXCL_STOP
1583+ }
1584+ else if (bytes_written != buf.size())
1585+ {
1586+ // LCOV_EXCL_START
1587+ string msg = "write error, requested " + to_string(buf.size()) + " B, but wrote only "
1588+ + to_string(bytes_written) + " B.";
1589+ throw_storage_exception(method_, msg, QFileDevice::FatalError);
1590+ // LCOV_EXCL_STOP
1591+ }
1592+ }
1593+ }
1594+ catch (std::exception const&)
1595+ {
1596+ abort_upload();
1597+ report_error(current_exception());
1598+ }
1599+}
1600+
1601+void LocalUploadJob::on_read_channel_finished()
1602+{
1603+ on_bytes_ready(); // In case there s still buffered data to be read.
1604+}
1605+
1606+void LocalUploadJob::abort_upload()
1607+{
1608+ state_ = cancelled;
1609+ disconnect(&read_socket_, nullptr, this, nullptr);
1610+ read_socket_.abort();
1611+ file_->close();
1612+ if (!use_linkat_)
1613+ {
1614+ // LCOV_EXCL_START
1615+ string filename = file_->fileName().toStdString();
1616+ ::unlink(filename.c_str()); // Don't leave any temp file behind.
1617+ // LCOV_EXCL_STOP
1618+ }
1619+ bytes_to_write_ = 0;
1620+}
1621
1622=== added file 'src/local-provider/LocalUploadJob.h'
1623--- src/local-provider/LocalUploadJob.h 1970-01-01 00:00:00 +0000
1624+++ src/local-provider/LocalUploadJob.h 2017-03-17 05:04:57 +0000
1625@@ -0,0 +1,79 @@
1626+/*
1627+ * Copyright (C) 2017 Canonical Ltd
1628+ *
1629+ * This program is free software: you can redistribute it and/or modify
1630+ * it under the terms of the GNU Lesser General Public License version 3 as
1631+ * published by the Free Software Foundation.
1632+ *
1633+ * This program is distributed in the hope that it will be useful,
1634+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1635+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1636+ * GNU Lesser General Public License for more details.
1637+ *
1638+ * You should have received a copy of the GNU Lesser General Public License
1639+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1640+ *
1641+ * Authors: Michi Henning <michi.henning@canonical.com>
1642+ */
1643+
1644+#pragma once
1645+
1646+#include <unity/storage/provider/UploadJob.h>
1647+
1648+#include <unity/util/ResourcePtr.h>
1649+
1650+#pragma GCC diagnostic push
1651+#pragma GCC diagnostic ignored "-Wctor-dtor-privacy"
1652+#pragma GCC diagnostic ignored "-Wswitch-default"
1653+#include <QFile>
1654+#include <QLocalSocket>
1655+#pragma GCC diagnostic pop
1656+
1657+class LocalProvider;
1658+
1659+class LocalUploadJob : public QObject, public unity::storage::provider::UploadJob
1660+{
1661+ Q_OBJECT
1662+public:
1663+ LocalUploadJob(std::shared_ptr<LocalProvider> const& provider, int64_t size, const std::string& method);
1664+
1665+ // create_file()
1666+ LocalUploadJob(std::shared_ptr<LocalProvider> const& provider,
1667+ std::string const& parent_id,
1668+ std::string const& name,
1669+ int64_t size,
1670+ bool allow_overwrite);
1671+ // update()
1672+ LocalUploadJob(std::shared_ptr<LocalProvider> const& provider,
1673+ std::string const& item_id,
1674+ int64_t size,
1675+ std::string const& old_etag);
1676+ virtual ~LocalUploadJob();
1677+
1678+ virtual boost::future<void> cancel() override;
1679+ virtual boost::future<unity::storage::provider::Item> finish() override;
1680+
1681+private Q_SLOTS:
1682+ void on_bytes_ready();
1683+ void on_read_channel_finished();
1684+
1685+private:
1686+ enum State { in_progress, finished, cancelled };
1687+
1688+ void prepare_channels();
1689+ void abort_upload();
1690+
1691+ std::shared_ptr<LocalProvider> const provider_;
1692+ int64_t const size_;
1693+ int64_t bytes_to_write_;
1694+ std::unique_ptr<QFile> file_;
1695+ QLocalSocket read_socket_;
1696+ std::string const method_;
1697+ State state_;
1698+ std::string item_id_;
1699+ std::string old_etag_; // Empty for create_file()
1700+ std::string parent_id_; // Empty for update()
1701+ bool allow_overwrite_; // Undefined for update()
1702+ unity::util::ResourcePtr<int, std::function<void(int)>> tmp_fd_;
1703+ bool use_linkat_;
1704+};
1705
1706=== added file 'src/local-provider/com.canonical.StorageFramework.Provider.Local.service.in'
1707--- src/local-provider/com.canonical.StorageFramework.Provider.Local.service.in 1970-01-01 00:00:00 +0000
1708+++ src/local-provider/com.canonical.StorageFramework.Provider.Local.service.in 2017-03-17 05:04:57 +0000
1709@@ -0,0 +1,3 @@
1710+[D-BUS Service]
1711+Name=com.canonical.StorageFramework.Provider.Local
1712+Exec=@CMAKE_INSTALL_FULL_LIBDIR@/@PROJECT_NAME@/storage-provider-local
1713
1714=== added file 'src/local-provider/main.cpp'
1715--- src/local-provider/main.cpp 1970-01-01 00:00:00 +0000
1716+++ src/local-provider/main.cpp 2017-03-17 05:04:57 +0000
1717@@ -0,0 +1,52 @@
1718+/*
1719+ * Copyright (C) 2017 Canonical Ltd
1720+ *
1721+ * This program is free software: you can redistribute it and/or modify
1722+ * it under the terms of the GNU Lesser General Public License version 3 as
1723+ * published by the Free Software Foundation.
1724+ *
1725+ * This program is distributed in the hope that it will be useful,
1726+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1727+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1728+ * GNU Lesser General Public License for more details.
1729+ *
1730+ * You should have received a copy of the GNU Lesser General Public License
1731+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1732+ *
1733+ * Authors: Michi Henning <michi.henning@canonical.com>
1734+ */
1735+
1736+#include <unity/storage/provider/Server.h>
1737+
1738+#include "LocalProvider.h"
1739+
1740+#include <boost/filesystem.hpp>
1741+
1742+#include <iostream>
1743+
1744+using namespace std;
1745+using namespace unity::storage::provider;
1746+
1747+int main(int argc, char* argv[])
1748+{
1749+ using namespace boost::filesystem;
1750+
1751+ string const bus_name = "com.canonical.StorageFramework.Provider.Local";
1752+ string const account_service_id = "";
1753+
1754+ string progname = argv[0];
1755+
1756+ try
1757+ {
1758+ progname = path(progname).filename().native();
1759+
1760+ Server<LocalProvider> server(bus_name, account_service_id);
1761+ server.init(argc, argv);
1762+ server.run();
1763+ }
1764+ catch (std::exception const& e)
1765+ {
1766+ cerr << progname << ": " << e.what() << endl;
1767+ return 1;
1768+ }
1769+}
1770
1771=== added file 'src/local-provider/utils.cpp'
1772--- src/local-provider/utils.cpp 1970-01-01 00:00:00 +0000
1773+++ src/local-provider/utils.cpp 2017-03-17 05:04:57 +0000
1774@@ -0,0 +1,146 @@
1775+/*
1776+ * Copyright (C) 2017 Canonical Ltd
1777+ *
1778+ * This program is free software: you can redistribute it and/or modify
1779+ * it under the terms of the GNU Lesser General Public License version 3 as
1780+ * published by the Free Software Foundation.
1781+ *
1782+ * This program is distributed in the hope that it will be useful,
1783+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1784+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1785+ * GNU Lesser General Public License for more details.
1786+ *
1787+ * You should have received a copy of the GNU Lesser General Public License
1788+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1789+ *
1790+ * Authors: Michi Henning <michi.henning@canonical.com>
1791+ */
1792+
1793+#include "utils.h"
1794+
1795+#include <unity/storage/internal/safe_strerror.h>
1796+#include <unity/storage/provider/Exceptions.h>
1797+
1798+#include <boost/algorithm/string.hpp>
1799+#include <boost/exception/enable_current_exception.hpp>
1800+
1801+#include <sys/stat.h>
1802+
1803+using namespace unity::storage::provider;
1804+using namespace std;
1805+
1806+// Return modification time in nanoseconds since the epoch.
1807+
1808+int64_t get_mtime_nsecs(string const& method, string const& path)
1809+{
1810+ using namespace unity::storage::internal;
1811+
1812+ struct stat st;
1813+ if (stat(path.c_str(), &st) == -1)
1814+ {
1815+ // LCOV_EXCL_START
1816+ string msg = method + ": cannot stat \"" + path + "\": " + safe_strerror(errno);
1817+ throw boost::enable_current_exception(ResourceException(msg, errno));
1818+ // LCOV_EXCL_STOP
1819+ }
1820+ return int64_t(st.st_mtim.tv_sec) * 1000000000 + st.st_mtim.tv_nsec;
1821+}
1822+
1823+// Return true if the path uses the temp file prefix.
1824+
1825+bool is_reserved_path(boost::filesystem::path const& path)
1826+{
1827+ string filename = path.filename().native();
1828+ return boost::starts_with(filename, TMPFILE_PREFIX);
1829+}
1830+
1831+// Check that name is a valid file or directory name, that is, has a single component
1832+// and is not "", ".", or "..". Also check that the name does not start with the
1833+// temp file prefix. Throw if the name is invalid.
1834+
1835+boost::filesystem::path sanitize(string const& method, string const& name)
1836+{
1837+ using namespace boost::filesystem;
1838+
1839+ path p = name;
1840+ if (!p.parent_path().empty())
1841+ {
1842+ // name contains more than one component.
1843+ string msg = method + ": name \"" + name + "\" cannot contain a slash";
1844+ throw boost::enable_current_exception(InvalidArgumentException(msg));
1845+ }
1846+ path filename = p.filename();
1847+ if (filename.empty() || filename == "." || filename == "..")
1848+ {
1849+ // Not an allowable file name.
1850+ string msg = method + ": invalid name: \"" + name + "\"";
1851+ throw boost::enable_current_exception(InvalidArgumentException(msg));
1852+ }
1853+ if (is_reserved_path(filename))
1854+ {
1855+ string msg = string(method + ": names beginning with \"") + TMPFILE_PREFIX + "\" are reserved";
1856+ throw boost::enable_current_exception(InvalidArgumentException(msg));
1857+ }
1858+ return p;
1859+}
1860+
1861+// Throw a StorageException that corresponds to a boost::filesystem_error.
1862+
1863+void throw_storage_exception(string const& method, boost::filesystem::filesystem_error const& e)
1864+{
1865+ using namespace boost::system::errc;
1866+
1867+ string msg = method + ": ";
1868+ string path1 = e.path1().native();
1869+ string path2 = e.path2().native();
1870+ if (!path2.empty())
1871+ {
1872+ msg += "src = \"" + path1 + "\", target = \"" + path2 + "\""; // LCOV_EXCL_LINE
1873+ }
1874+ else
1875+ {
1876+ msg += "\"" + path1 + "\"";
1877+ }
1878+ msg += string(": ") + e.what();
1879+ switch (e.code().value())
1880+ {
1881+ case permission_denied:
1882+ case operation_not_permitted:
1883+ throw boost::enable_current_exception(PermissionException(msg));
1884+ case no_such_file_or_directory:
1885+ throw boost::enable_current_exception(NotExistsException(msg, e.path1().native()));
1886+ // LCOV_EXCL_START
1887+ case file_exists:
1888+ throw boost::enable_current_exception(
1889+ ExistsException(msg, e.path1().native(), e.path1().filename().native()));
1890+ case no_space_on_device:
1891+ throw boost::enable_current_exception(QuotaException(msg));
1892+ default:
1893+ throw boost::enable_current_exception(ResourceException(msg, e.code().value()));
1894+ // LCOV_EXCL_STOP
1895+ }
1896+}
1897+
1898+// Throw a storage exception that corresponds to a FileError.
1899+
1900+void throw_storage_exception(string const& method, string const& msg, QFileDevice::FileError e)
1901+{
1902+ string const error_msg = method + ": " + msg;
1903+ switch (e)
1904+ {
1905+ case QFileDevice::NoError:
1906+ abort(); // LCOV_EXCL_LINE // Precondition violation
1907+ break;
1908+ default:
1909+ throw ResourceException(error_msg + " (QFileDevice::FileError = " + to_string(e) + ")", e);
1910+ }
1911+}
1912+
1913+// Throw a storage exception that corresponds to a LocalSocketError.
1914+
1915+// LCOV_EXCL_START
1916+void throw_storage_exception(string const& method, string const& msg, QLocalSocket::LocalSocketError e)
1917+{
1918+ throw ResourceException(method + ": " + msg + " (QLocalSocket::LocalSocketError = " + to_string(e) + ")", e);
1919+}
1920+// LCOV_EXCL_STOP
1921
1922=== added file 'src/local-provider/utils.h'
1923--- src/local-provider/utils.h 1970-01-01 00:00:00 +0000
1924+++ src/local-provider/utils.h 2017-03-17 05:04:57 +0000
1925@@ -0,0 +1,45 @@
1926+/*
1927+ * Copyright (C) 2017 Canonical Ltd
1928+ *
1929+ * This program is free software: you can redistribute it and/or modify
1930+ * it under the terms of the GNU Lesser General Public License version 3 as
1931+ * published by the Free Software Foundation.
1932+ *
1933+ * This program is distributed in the hope that it will be useful,
1934+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1935+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1936+ * GNU Lesser General Public License for more details.
1937+ *
1938+ * You should have received a copy of the GNU Lesser General Public License
1939+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1940+ *
1941+ * Authors: Michi Henning <michi.henning@canonical.com>
1942+ */
1943+
1944+#pragma once
1945+
1946+#include <boost/filesystem.hpp>
1947+
1948+#pragma GCC diagnostic push
1949+#pragma GCC diagnostic ignored "-Wctor-dtor-privacy"
1950+#include <QFileDevice>
1951+#include <QLocalSocket>
1952+#include <QString>
1953+#pragma GCC diagnostic pop
1954+
1955+#include <string>
1956+
1957+constexpr char const* TMPFILE_PREFIX = ".storage-framework";
1958+
1959+int64_t get_mtime_nsecs(std::string const& method, std::string const& path);
1960+bool is_reserved_path(boost::filesystem::path const& path);
1961+boost::filesystem::path sanitize(std::string const& method, std::string const& name);
1962+
1963+[[ noreturn ]]
1964+void throw_storage_exception(std::string const& method, boost::filesystem::filesystem_error const& e);
1965+
1966+[[ noreturn ]]
1967+void throw_storage_exception(std::string const& method, std::string const& msg, QFileDevice::FileError e);
1968+
1969+[[ noreturn ]]
1970+void throw_storage_exception(std::string const& method, std::string const& msg, QLocalSocket::LocalSocketError e);
1971
1972=== modified file 'src/qt/internal/RuntimeImpl.cpp'
1973--- src/qt/internal/RuntimeImpl.cpp 2016-11-21 13:29:39 +0000
1974+++ src/qt/internal/RuntimeImpl.cpp 2017-03-17 05:04:57 +0000
1975@@ -24,9 +24,11 @@
1976 #include <unity/storage/qt/internal/AccountImpl.h>
1977 #include <unity/storage/qt/internal/AccountsJobImpl.h>
1978 #include <unity/storage/qt/internal/StorageErrorImpl.h>
1979+#include <unity/storage/qt/Downloader.h>
1980 #include <unity/storage/qt/ItemJob.h>
1981 #include <unity/storage/qt/ItemListJob.h>
1982 #include <unity/storage/qt/Runtime.h>
1983+#include <unity/storage/qt/Uploader.h>
1984 #include <unity/storage/qt/VoidJob.h>
1985 #include <unity/storage/registry/Registry.h>
1986
1987@@ -51,10 +53,12 @@
1988 qRegisterMetaType<unity::storage::qt::AccountsJob::Status>();
1989 qRegisterMetaType<unity::storage::qt::Account>();
1990 qRegisterMetaType<QList<unity::storage::qt::Account>>();
1991+ qRegisterMetaType<unity::storage::qt::Downloader::Status>();
1992 qRegisterMetaType<unity::storage::qt::Item>();
1993 qRegisterMetaType<QList<unity::storage::qt::Item>>();
1994 qRegisterMetaType<unity::storage::qt::ItemJob::Status>();
1995 qRegisterMetaType<unity::storage::qt::ItemListJob::Status>();
1996+ qRegisterMetaType<unity::storage::qt::Uploader::Status>();
1997 qRegisterMetaType<unity::storage::qt::VoidJob::Status>();
1998
1999 qDBusRegisterMetaType<unity::storage::internal::ItemMetadata>();
2000
2001=== modified file 'src/qt/internal/StorageErrorImpl.cpp'
2002--- src/qt/internal/StorageErrorImpl.cpp 2017-01-24 06:12:09 +0000
2003+++ src/qt/internal/StorageErrorImpl.cpp 2017-03-17 05:04:57 +0000
2004@@ -78,7 +78,6 @@
2005 || type == StorageError::Type::Cancelled
2006 || type == StorageError::Type::LogicError
2007 || type == StorageError::Type::InvalidArgument);
2008- assert(!msg.isEmpty());
2009
2010 message_ = msg;
2011 }
2012@@ -87,7 +86,6 @@
2013 : StorageErrorImpl(type)
2014 {
2015 assert(type == StorageError::Type::NotExists);
2016- assert(!msg.isEmpty());
2017
2018 message_ = msg;
2019 item_id_ = key;
2020@@ -104,9 +102,6 @@
2021 : StorageErrorImpl(type)
2022 {
2023 assert(type == StorageError::Type::Exists);
2024- assert(!msg.isEmpty());
2025- assert(!item_id.isEmpty());
2026- assert(!item_name.isEmpty());
2027
2028 message_ = msg;
2029 item_id_ = item_id;
2030@@ -117,7 +112,6 @@
2031 : StorageErrorImpl(type)
2032 {
2033 assert(type == StorageError::Type::ResourceError);
2034- assert(!msg.isEmpty());
2035
2036 message_ = msg;
2037 error_code_ = error_code;
2038
2039=== modified file 'tests/CMakeLists.txt'
2040--- tests/CMakeLists.txt 2017-03-07 10:23:24 +0000
2041+++ tests/CMakeLists.txt 2017-03-17 05:04:57 +0000
2042@@ -8,6 +8,7 @@
2043 set(unit_test_dirs
2044 registry
2045 local-client
2046+ local-provider
2047 remote-client
2048 remote-client-v1
2049 provider-AccountData
2050
2051=== added directory 'tests/local-provider'
2052=== added file 'tests/local-provider/CMakeLists.txt'
2053--- tests/local-provider/CMakeLists.txt 1970-01-01 00:00:00 +0000
2054+++ tests/local-provider/CMakeLists.txt 2017-03-17 05:04:57 +0000
2055@@ -0,0 +1,18 @@
2056+add_executable(local-provider_test local-provider_test.cpp)
2057+
2058+add_definitions(-DTEST_DIR="${CMAKE_CURRENT_BINARY_DIR}" -DBOOST_THREAD_VERSION=4)
2059+
2060+target_link_libraries(local-provider_test
2061+ local-provider-lib
2062+ storage-framework-provider
2063+ storage-framework-qt-client-v2
2064+ Qt5::Test
2065+ ${Boost_LIBRARIES}
2066+ ${GLIB_DEPS_LIBRARIES}
2067+ ${GIO_DEPS_LIBRARIES}
2068+ testutils
2069+ gtest
2070+)
2071+add_test(local-provider local-provider_test)
2072+
2073+set(UNIT_TEST_TARGETS ${UNIT_TEST_TARGETS} PARENT_SCOPE)
2074
2075=== added file 'tests/local-provider/local-provider_test.cpp'
2076--- tests/local-provider/local-provider_test.cpp 1970-01-01 00:00:00 +0000
2077+++ tests/local-provider/local-provider_test.cpp 2017-03-17 05:04:57 +0000
2078@@ -0,0 +1,1473 @@
2079+/*
2080+ * Copyright (C) 2017 Canonical Ltd
2081+ *
2082+ * This program is free software: you can redistribute it and/or modify
2083+ * it under the terms of the GNU Lesser General Public License version 3 as
2084+ * published by the Free Software Foundation.
2085+ *
2086+ * This program is distributed in the hope that it will be useful,
2087+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
2088+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2089+ * GNU Lesser General Public License for more details.
2090+ *
2091+ * You should have received a copy of the GNU Lesser General Public License
2092+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
2093+ *
2094+ * Authors: Michi Henning <michi.henning@canonical.com>
2095+ */
2096+
2097+#include "../../src/local-provider/LocalDownloadJob.h"
2098+#include "../../src/local-provider/LocalProvider.h"
2099+#include "../../src/local-provider/LocalUploadJob.h"
2100+
2101+#include <unity/storage/provider/DownloadJob.h>
2102+#include <unity/storage/provider/Exceptions.h>
2103+#include <unity/storage/provider/Server.h>
2104+#include <unity/storage/qt/client-api.h>
2105+#include <utils/env_var_guard.h>
2106+#include <utils/ProviderFixture.h>
2107+
2108+#include <boost/algorithm/string.hpp>
2109+#include <gtest/gtest.h>
2110+#include <QCoreApplication>
2111+#include <QSignalSpy>
2112+
2113+#include <chrono>
2114+#include <regex>
2115+
2116+#include <fcntl.h>
2117+
2118+using namespace unity::storage;
2119+using namespace std;
2120+
2121+namespace
2122+{
2123+
2124+int64_t nanosecs_now()
2125+{
2126+ return chrono::system_clock::now().time_since_epoch() / chrono::nanoseconds(1);
2127+}
2128+
2129+string const ROOT_DIR = TEST_DIR "/data";
2130+
2131+class LocalProviderTest : public ProviderFixture
2132+{
2133+protected:
2134+ void SetUp() override
2135+ {
2136+ boost::filesystem::remove_all(ROOT_DIR);
2137+ boost::filesystem::create_directory(ROOT_DIR);
2138+
2139+ ProviderFixture::SetUp();
2140+ runtime_.reset(new qt::Runtime(connection()));
2141+ acc_ = runtime_->make_test_account(service_connection_->baseService(), object_path());
2142+ }
2143+
2144+ void TearDown() override
2145+ {
2146+ runtime_.reset();
2147+ ProviderFixture::TearDown();
2148+ }
2149+
2150+ unique_ptr<qt::Runtime> runtime_;
2151+ qt::Account acc_;
2152+};
2153+
2154+constexpr int SIGNAL_WAIT_TIME = 30000;
2155+
2156+template <typename Job>
2157+void wait(Job* job)
2158+{
2159+ QSignalSpy spy(job, &Job::statusChanged);
2160+ while (job->status() == Job::Loading)
2161+ {
2162+ if (!spy.wait(SIGNAL_WAIT_TIME))
2163+ {
2164+ throw runtime_error("Wait for statusChanged signal timed out");
2165+ }
2166+ }
2167+}
2168+
2169+qt::Item get_root(qt::Account const& account)
2170+{
2171+ unique_ptr<qt::ItemListJob> j(account.roots());
2172+ assert(j->isValid());
2173+ QSignalSpy ready_spy(j.get(), &qt::ItemListJob::itemsReady);
2174+ assert(ready_spy.wait(SIGNAL_WAIT_TIME));
2175+ auto arg = ready_spy.takeFirst();
2176+ auto items = qvariant_cast<QList<qt::Item>>(arg.at(0));
2177+ assert(items.size() == 1);
2178+ return items[0];
2179+}
2180+
2181+QList<qt::Item> get_items(qt::ItemListJob *job)
2182+{
2183+ QList<qt::Item> items;
2184+ auto connection = QObject::connect(
2185+ job, &qt::ItemListJob::itemsReady,
2186+ [&](QList<qt::Item> const& new_items)
2187+ {
2188+ items.append(new_items);
2189+ });
2190+ try
2191+ {
2192+ wait(job);
2193+ }
2194+ catch (...)
2195+ {
2196+ QObject::disconnect(connection);
2197+ throw;
2198+ }
2199+ QObject::disconnect(connection);
2200+ return items;
2201+}
2202+
2203+const string file_contents =
2204+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do "
2205+ "eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut "
2206+ "enim ad minim veniam, quis nostrud exercitation ullamco laboris "
2207+ "nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor "
2208+ "in reprehenderit in voluptate velit esse cillum dolore eu fugiat "
2209+ "nulla pariatur. Excepteur sint occaecat cupidatat non proident, "
2210+ "sunt in culpa qui officia deserunt mollit anim id est laborum.\n";
2211+
2212+} // namespace
2213+
2214+TEST(Directories, env_vars)
2215+{
2216+ // These tests cause the constructor to throw, so we instantiate the provider directly.
2217+
2218+ {
2219+ EnvVarGuard env("SF_LOCAL_PROVIDER_ROOT", "/no_such_dir");
2220+
2221+ try
2222+ {
2223+ LocalProvider();
2224+ FAIL();
2225+ }
2226+ catch (provider::InvalidArgumentException const& e)
2227+ {
2228+ EXPECT_STREQ("InvalidArgumentException: LocalProvider(): Environment variable "
2229+ "SF_LOCAL_PROVIDER_ROOT must denote an existing directory",
2230+ e.what());
2231+ }
2232+ }
2233+
2234+ {
2235+ EnvVarGuard env("SF_LOCAL_PROVIDER_ROOT", TEST_DIR "/Makefile");
2236+
2237+ try
2238+ {
2239+ LocalProvider();
2240+ FAIL();
2241+ }
2242+ catch (provider::InvalidArgumentException const& e)
2243+ {
2244+ EXPECT_STREQ("InvalidArgumentException: LocalProvider(): Environment variable "
2245+ "SF_LOCAL_PROVIDER_ROOT must denote an existing directory",
2246+ e.what());
2247+ }
2248+ }
2249+
2250+ {
2251+ string const dir = TEST_DIR "/noperm";
2252+
2253+ mkdir(dir.c_str(), 0555);
2254+ ASSERT_EQ(0, chmod(dir.c_str(), 0555)); // In case dir was there already.
2255+
2256+ EnvVarGuard env1("SF_LOCAL_PROVIDER_ROOT", nullptr);
2257+ EnvVarGuard env2("XDG_DATA_HOME", dir.c_str());
2258+
2259+ using namespace boost::filesystem;
2260+
2261+ try
2262+ {
2263+ LocalProvider();
2264+ ASSERT_EQ(0, chmod(dir.c_str(), 0775));
2265+ remove_all(dir);
2266+ FAIL();
2267+ }
2268+ catch (provider::PermissionException const& e)
2269+ {
2270+ EXPECT_EQ(string("PermissionException: LocalProvider(): \"") + dir + "/storage-framework\": "
2271+ "boost::filesystem::create_directories: Permission denied: \"" + dir + "/storage-framework\"",
2272+ e.what());
2273+ }
2274+
2275+ ASSERT_EQ(0, chmod(dir.c_str(), 0775));
2276+ ASSERT_TRUE(remove_all(dir));
2277+
2278+ // Try again, which must succeed now (for coverage).
2279+ LocalProvider();
2280+ ASSERT_TRUE(is_directory(dir + "/storage-framework/local"));
2281+ ASSERT_TRUE(remove_all(dir));
2282+ }
2283+
2284+ {
2285+ string const dir = TEST_DIR "/snap_user_common";
2286+ mkdir(dir.c_str(), 0775);
2287+
2288+ EnvVarGuard env1("SF_LOCAL_PROVIDER_ROOT", nullptr);
2289+ EnvVarGuard env2("SNAP_USER_COMMON", dir.c_str());
2290+
2291+ using namespace boost::filesystem;
2292+
2293+ LocalProvider();
2294+ ASSERT_TRUE(exists(dir + "/storage-framework/local"));
2295+ ASSERT_TRUE(remove_all(dir));
2296+ }
2297+}
2298+
2299+TEST_F(LocalProviderTest, basic)
2300+{
2301+ using namespace unity::storage::qt;
2302+
2303+ set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
2304+
2305+ // Basic sanity check, get the root.
2306+ unique_ptr<ItemListJob> j(acc_.roots());
2307+ EXPECT_TRUE(j->isValid());
2308+ QSignalSpy ready_spy(j.get(), &ItemListJob::itemsReady);
2309+ ASSERT_TRUE(ready_spy.wait(SIGNAL_WAIT_TIME));
2310+ ASSERT_EQ(1, ready_spy.count());
2311+ auto arg = ready_spy.takeFirst();
2312+ auto items = qvariant_cast<QList<Item>>(arg.at(0));
2313+ ASSERT_EQ(1, items.size());
2314+
2315+ // Check contents of returned item.
2316+ auto root = items[0];
2317+ EXPECT_TRUE(root.isValid());
2318+ EXPECT_EQ(Item::Type::Root, root.type());
2319+ EXPECT_EQ(ROOT_DIR, root.itemId().toStdString());
2320+ EXPECT_EQ("/", root.name());
2321+ EXPECT_EQ("", root.etag());
2322+ EXPECT_EQ(QList<QString>(), root.parentIds());
2323+ qDebug() << root.lastModifiedTime();
2324+ EXPECT_TRUE(root.lastModifiedTime().isValid());
2325+ EXPECT_EQ(acc_, root.account());
2326+
2327+ ASSERT_EQ(5, root.metadata().size());
2328+ auto free_space_bytes = root.metadata().value("free_space_bytes").toULongLong();
2329+ cout << "free_space_bytes: " << free_space_bytes << endl;
2330+ EXPECT_GT(free_space_bytes, 0);
2331+ auto used_space_bytes = root.metadata().value("used_space_bytes").toULongLong();
2332+ cout << "used_space_bytes: " << used_space_bytes << endl;
2333+ EXPECT_GT(used_space_bytes, 0);
2334+ auto content_type = root.metadata().value("content_type").toString();
2335+ EXPECT_EQ("inode/directory", content_type);
2336+ auto writable = root.metadata().value("writable").toBool();
2337+ EXPECT_TRUE(writable);
2338+
2339+ // yyyy-mm-ddThh:mm:ssZ
2340+ string const date_time_fmt = "^[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]T[0-9][0-9]:[0-9][0-9]:[0-9][0-9]Z$";
2341+ string mtime = root.metadata().value("last_modified_time").toString().toStdString();
2342+ cout << "last_modified_time: " << mtime << endl;
2343+ regex re(date_time_fmt);
2344+ EXPECT_TRUE(regex_match(mtime, re));
2345+}
2346+
2347+TEST_F(LocalProviderTest, create_folder)
2348+{
2349+ {
2350+ using namespace unity::storage::qt;
2351+
2352+ set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
2353+
2354+ auto root = get_root(acc_);
2355+ unique_ptr<ItemJob> job(root.createFolder("child"));
2356+ wait(job.get());
2357+ ASSERT_EQ(ItemJob::Finished, job->status()) << job->error().errorString().toStdString();
2358+
2359+ Item child = job->item();
2360+ EXPECT_EQ(ROOT_DIR + "/child", child.itemId().toStdString());
2361+ EXPECT_EQ("child", child.name().toStdString());
2362+ ASSERT_EQ(1, child.parentIds().size());
2363+ EXPECT_EQ(ROOT_DIR, child.parentIds().at(0).toStdString());
2364+ EXPECT_EQ("", child.etag());
2365+ EXPECT_EQ(Item::Type::Folder, child.type());
2366+ EXPECT_EQ(5, child.metadata().size());
2367+
2368+ struct stat st;
2369+ ASSERT_EQ(0, stat(child.itemId().toStdString().c_str(), &st));
2370+ EXPECT_TRUE(S_ISDIR(st.st_mode));
2371+
2372+ // Again, to get coverage for a StorageException caught in invoke_async().
2373+ job.reset(root.createFolder("child"));
2374+ wait(job.get());
2375+ ASSERT_EQ(ItemJob::Error, job->status()) << job->error().errorString().toStdString();
2376+ EXPECT_EQ(string("Exists: create_folder(): \"") + ROOT_DIR + "/child\" exists already",
2377+ job->error().errorString().toStdString());
2378+
2379+ // Again, without write permission on the root dir, to get coverage for a filesystem_error in invoke_async().
2380+ ASSERT_EQ(0, ::rmdir((ROOT_DIR + "/child").c_str()));
2381+ ASSERT_EQ(0, ::chmod(ROOT_DIR.c_str(), 0555));
2382+ job.reset(root.createFolder("child"));
2383+ wait(job.get());
2384+ ::chmod(ROOT_DIR.c_str(), 0755);
2385+ ASSERT_EQ(ItemJob::Error, job->status()) << job->error().errorString().toStdString();
2386+ EXPECT_EQ(string("PermissionDenied: create_folder(): \"") + ROOT_DIR
2387+ + "/child\": boost::filesystem::create_directory: Permission denied: \"" + ROOT_DIR + "/child\"",
2388+ job->error().errorString().toStdString());
2389+ }
2390+}
2391+
2392+TEST_F(LocalProviderTest, delete_item)
2393+{
2394+ {
2395+ using namespace unity::storage::qt;
2396+
2397+ set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
2398+
2399+ auto root = get_root(acc_);
2400+ unique_ptr<ItemJob> job(root.createFolder("child"));
2401+ wait(job.get());
2402+ ASSERT_EQ(ItemJob::Finished, job->status()) << job->error().errorString().toStdString();
2403+
2404+ Item child = job->item();
2405+ unique_ptr<VoidJob> delete_job(child.deleteItem());
2406+ wait(delete_job.get());
2407+ ASSERT_EQ(ItemJob::Finished, delete_job->status()) << delete_job->error().errorString().toStdString();
2408+
2409+ struct stat st;
2410+ ASSERT_EQ(-1, stat(child.itemId().toStdString().c_str(), &st));
2411+ EXPECT_EQ(ENOENT, errno);
2412+ }
2413+}
2414+
2415+TEST_F(LocalProviderTest, delete_item_noperm)
2416+{
2417+ {
2418+ using namespace unity::storage::qt;
2419+
2420+ set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
2421+
2422+ auto root = get_root(acc_);
2423+ unique_ptr<ItemJob> job(root.createFolder("child"));
2424+ wait(job.get());
2425+ ASSERT_EQ(ItemJob::Finished, job->status()) << job->error().errorString().toStdString();
2426+ }
2427+}
2428+
2429+TEST_F(LocalProviderTest, delete_root)
2430+{
2431+ // Client-side API does not allow us to try to delete the root, so we talk to the provider directly.
2432+ auto p = make_shared<LocalProvider>();
2433+
2434+ auto fut = p->delete_item(ROOT_DIR, provider::Context());
2435+ try
2436+ {
2437+ fut.get();
2438+ FAIL();
2439+ }
2440+ catch (provider::LogicException const& e)
2441+ {
2442+ EXPECT_STREQ("LogicException: delete_item(): cannot delete root", e.what());
2443+ }
2444+}
2445+
2446+TEST_F(LocalProviderTest, metadata)
2447+{
2448+ // Client-side API does not call the Metadata DBus method (except as part of parents()),
2449+ // so we talk to the provider directly.
2450+ auto p = make_shared<LocalProvider>();
2451+
2452+ auto fut = p->metadata(ROOT_DIR, {}, provider::Context());
2453+ auto item = fut.get();
2454+ EXPECT_EQ(5, item.metadata.size());
2455+
2456+ // Again, to get coverage for the "not file or folder" case in make_item().
2457+ ASSERT_EQ(0, mknod((ROOT_DIR + "/pipe").c_str(), S_IFIFO | 06666, 0));
2458+ try
2459+ {
2460+ auto fut = p->metadata(ROOT_DIR + "/pipe", {}, provider::Context());
2461+ fut.get();
2462+ FAIL();
2463+ }
2464+ catch (provider::NotExistsException const& e)
2465+ {
2466+ EXPECT_EQ(string("NotExistsException: metadata(): \"") + ROOT_DIR + "/pipe\" is neither a file nor a folder",
2467+ e.what());
2468+ }
2469+}
2470+
2471+TEST_F(LocalProviderTest, lookup)
2472+{
2473+ using namespace unity::storage::qt;
2474+
2475+ set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
2476+
2477+ auto root = get_root(acc_);
2478+ {
2479+ unique_ptr<ItemJob> job(root.createFolder("child"));
2480+ wait(job.get());
2481+ ASSERT_EQ(ItemJob::Finished, job->status()) << job->error().errorString().toStdString();
2482+ }
2483+
2484+ unique_ptr<ItemListJob> job(root.lookup("child"));
2485+ auto items = get_items(job.get());
2486+ ASSERT_EQ(1, items.size());
2487+ auto child = items.at(0);
2488+
2489+ EXPECT_EQ(ROOT_DIR + "/child", child.itemId().toStdString());
2490+ EXPECT_EQ("child", child.name().toStdString());
2491+ ASSERT_EQ(1, child.parentIds().size());
2492+ EXPECT_EQ(ROOT_DIR, child.parentIds().at(0).toStdString());
2493+ EXPECT_EQ("", child.etag());
2494+ EXPECT_EQ(Item::Type::Folder, child.type());
2495+ EXPECT_EQ(5, child.metadata().size());
2496+
2497+ // Remove the child again and try the lookup once more.
2498+ ASSERT_EQ(0, rmdir((ROOT_DIR + "/child").c_str()));
2499+
2500+ job.reset(root.lookup("child"));
2501+ wait(job.get());
2502+ EXPECT_EQ(ItemJob::Error, job->status());
2503+ EXPECT_EQ(string("NotExists: lookup(): \"") + ROOT_DIR + "/child\": boost::filesystem::canonical: "
2504+ + "No such file or directory: \"" + ROOT_DIR + "/child\"",
2505+ job->error().errorString().toStdString());
2506+}
2507+
2508+TEST_F(LocalProviderTest, list)
2509+{
2510+ using namespace unity::storage::qt;
2511+
2512+ set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
2513+
2514+ auto root = get_root(acc_);
2515+ {
2516+ unique_ptr<ItemJob> job(root.createFolder("child"));
2517+ wait(job.get());
2518+ ASSERT_EQ(ItemJob::Finished, job->status()) << job->error().errorString().toStdString();
2519+ }
2520+
2521+ // Make a weird item that will be ignored (for coverage).
2522+ ASSERT_EQ(0, mknod((ROOT_DIR + "/pipe").c_str(), S_IFIFO | 06666, 0));
2523+
2524+ // Make a file that starts with the temp file prefix (for coverage).
2525+ int fd = creat((ROOT_DIR + "/.storage-framework").c_str(), 0755);
2526+ ASSERT_GT(fd, 0);
2527+ close(fd);
2528+
2529+ unique_ptr<ItemListJob> job(root.list());
2530+ auto items = get_items(job.get());
2531+ ASSERT_EQ(1, items.size());
2532+ auto child = items.at(0);
2533+
2534+ EXPECT_EQ(ROOT_DIR + "/child", child.itemId().toStdString());
2535+ EXPECT_EQ("child", child.name().toStdString());
2536+ ASSERT_EQ(1, child.parentIds().size());
2537+ EXPECT_EQ(ROOT_DIR, child.parentIds().at(0).toStdString());
2538+ EXPECT_EQ("", child.etag());
2539+ EXPECT_EQ(Item::Type::Folder, child.type());
2540+ EXPECT_EQ(5, child.metadata().size());
2541+}
2542+
2543+void make_hierarchy()
2544+{
2545+ // Make a small tree so we have something to test with for move() and copy().
2546+ ASSERT_EQ(0, mkdir((ROOT_DIR + "/a").c_str(), 0755));
2547+ ASSERT_EQ(0, mkdir((ROOT_DIR + "/a/b").c_str(), 0755));
2548+ string cmd = string("echo hello >") + ROOT_DIR + "/hello";
2549+ ASSERT_EQ(0, system(cmd.c_str()));
2550+ cmd = string("echo text >") + ROOT_DIR + "/a/foo.txt";
2551+ ASSERT_EQ(0, system(cmd.c_str()));
2552+ ASSERT_EQ(0, mknod((ROOT_DIR + "/a/pipe").c_str(), S_IFIFO | 06666, 0));
2553+ ASSERT_EQ(0, mkdir((ROOT_DIR + "/a/.storage-framework-").c_str(), 0755));
2554+ ASSERT_EQ(0, mkdir((ROOT_DIR + "/a/b/.storage-framework-").c_str(), 0755));
2555+ ASSERT_EQ(0, mknod((ROOT_DIR + "/a/b/pipe").c_str(), S_IFIFO | 06666, 0));
2556+}
2557+
2558+TEST_F(LocalProviderTest, move)
2559+{
2560+ using namespace unity::storage::qt;
2561+
2562+ set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
2563+
2564+ auto start_time = nanosecs_now();
2565+
2566+ make_hierarchy();
2567+
2568+ auto root = get_root(acc_);
2569+
2570+ qt::Item hello;
2571+ {
2572+ unique_ptr<ItemListJob> job(root.lookup("hello"));
2573+ auto items = get_items(job.get());
2574+ ASSERT_EQ(ItemListJob::Finished, job->status()) << job->error().errorString().toStdString();
2575+ ASSERT_EQ(1, items.size());
2576+ hello = items.at(0);
2577+ }
2578+
2579+ struct stat st;
2580+ ASSERT_EQ(0, stat(hello.itemId().toStdString().c_str(), &st));
2581+ auto old_ino = st.st_ino;
2582+
2583+ // Check metadata.
2584+ EXPECT_EQ("hello", hello.name());
2585+ ASSERT_EQ(1, hello.parentIds().size());
2586+ EXPECT_EQ(ROOT_DIR, hello.parentIds().at(0).toStdString());
2587+ EXPECT_EQ(Item::Type::File, hello.type());
2588+
2589+ ASSERT_EQ(6, hello.metadata().size());
2590+ auto free_space_bytes = hello.metadata().value("free_space_bytes").toLongLong();
2591+ cout << "free_space_bytes: " << free_space_bytes << endl;
2592+ EXPECT_GT(free_space_bytes, 0);
2593+ auto used_space_bytes = hello.metadata().value("used_space_bytes").toLongLong();
2594+ cout << "used_space_bytes: " << used_space_bytes << endl;
2595+ EXPECT_GT(used_space_bytes, 0);
2596+ auto content_type = hello.metadata().value("content_type").toString();
2597+ EXPECT_EQ("application/octet-stream", content_type);
2598+ auto writable = hello.metadata().value("writable").toBool();
2599+ EXPECT_TRUE(writable);
2600+ auto size = hello.metadata().value("size_in_bytes").toLongLong();
2601+ EXPECT_EQ(6, size);
2602+
2603+ // yyyy-mm-ddThh:mm:ssZ
2604+ string const date_time_fmt = "^[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]T[0-9][0-9]:[0-9][0-9]:[0-9][0-9]Z$";
2605+ string date_time = hello.metadata().value("last_modified_time").toString().toStdString();
2606+ cout << "last_modified_time: " << date_time << endl;
2607+ regex re(date_time_fmt);
2608+ EXPECT_TRUE(regex_match(date_time, re));
2609+
2610+ // Check that the file was modified in the last two seconds.
2611+ // Because the system clock can tick a lot more frequently than the file system time stamp,
2612+ // we allow the mtime to be up to one second *earlier* than the time we started the operation.
2613+ string mtime_str = hello.etag().toStdString();
2614+ char* end;
2615+ int64_t mtime = strtoll(mtime_str.c_str(), &end, 10);
2616+ EXPECT_LE(start_time - 1000000000, mtime);
2617+ EXPECT_LT(mtime, start_time + 2000000000);
2618+
2619+ // Move hello -> world
2620+ qt::Item world;
2621+ {
2622+ unique_ptr<ItemJob> job(hello.move(root, "world"));
2623+ wait(job.get());
2624+ ASSERT_EQ(ItemJob::Finished, job->status()) << job->error().errorString().toStdString();
2625+ world = job->item();
2626+ }
2627+ EXPECT_FALSE(boost::filesystem::exists(hello.itemId().toStdString()));
2628+ EXPECT_EQ(ROOT_DIR + "/world", world.itemId().toStdString());
2629+
2630+ ASSERT_EQ(0, stat(world.itemId().toStdString().c_str(), &st));
2631+ auto new_ino = st.st_ino;
2632+ EXPECT_EQ(old_ino, new_ino);
2633+
2634+ // For coverage: try moving world -> a (which must fail)
2635+ unique_ptr<ItemJob> job(world.move(root, "a"));
2636+ wait(job.get());
2637+ ASSERT_EQ(ItemJob::Error, job->status()) << job->error().errorString().toStdString();
2638+ EXPECT_EQ(string("Exists: move(): \"") + ROOT_DIR + "/a\" exists already",
2639+ job->error().errorString().toStdString());
2640+}
2641+
2642+TEST_F(LocalProviderTest, copy_file)
2643+{
2644+ using namespace unity::storage::qt;
2645+
2646+ set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
2647+
2648+ make_hierarchy();
2649+
2650+ auto root = get_root(acc_);
2651+
2652+ // Copy hello -> world
2653+ qt::Item hello;
2654+ {
2655+ unique_ptr<ItemListJob> job(root.lookup("hello"));
2656+ auto items = get_items(job.get());
2657+ ASSERT_EQ(ItemListJob::Finished, job->status()) << job->error().errorString().toStdString();
2658+ ASSERT_EQ(1, items.size());
2659+ hello = items.at(0);
2660+ }
2661+
2662+ struct stat st;
2663+ ASSERT_EQ(0, stat(hello.itemId().toStdString().c_str(), &st));
2664+ auto old_ino = st.st_ino;
2665+
2666+ qt::Item world;
2667+ {
2668+ unique_ptr<ItemJob> job(hello.copy(root, "world"));
2669+ wait(job.get());
2670+ ASSERT_EQ(ItemJob::Finished, job->status()) << job->error().errorString().toStdString();
2671+ world = job->item();
2672+ }
2673+ EXPECT_TRUE(boost::filesystem::exists(hello.itemId().toStdString()));
2674+ EXPECT_EQ(ROOT_DIR + "/world", world.itemId().toStdString());
2675+
2676+ ASSERT_EQ(0, stat(world.itemId().toStdString().c_str(), &st));
2677+ auto new_ino = st.st_ino;
2678+ EXPECT_NE(old_ino, new_ino);
2679+}
2680+
2681+TEST_F(LocalProviderTest, copy_tree)
2682+{
2683+ using namespace unity::storage::qt;
2684+ using namespace boost::filesystem;
2685+
2686+ set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
2687+
2688+ make_hierarchy();
2689+
2690+ auto root = get_root(acc_);
2691+
2692+ // Copy a -> c
2693+ qt::Item a;
2694+ {
2695+ unique_ptr<ItemListJob> job(root.lookup("a"));
2696+ auto items = get_items(job.get());
2697+ ASSERT_EQ(ItemListJob::Finished, job->status()) << job->error().errorString().toStdString();
2698+ ASSERT_EQ(1, items.size());
2699+ a = items.at(0);
2700+ }
2701+
2702+ qt::Item c;
2703+ {
2704+ unique_ptr<ItemJob> job(a.copy(root, "c"));
2705+ wait(job.get());
2706+ ASSERT_EQ(ItemJob::Finished, job->status()) << job->error().errorString().toStdString();
2707+ c = job->item();
2708+ }
2709+ EXPECT_TRUE(exists(c.itemId().toStdString()));
2710+
2711+ // Check that we only copied regular files and directories, but not a pipe or anything starting with
2712+ // the temp file prefix.
2713+ EXPECT_TRUE(exists(ROOT_DIR + "/c/b"));
2714+ EXPECT_TRUE(exists(ROOT_DIR + "/c/foo.txt"));
2715+ EXPECT_FALSE(exists(ROOT_DIR + "/c/pipe"));
2716+ EXPECT_FALSE(exists(ROOT_DIR + "/c/storage-framework-"));
2717+ EXPECT_FALSE(exists(ROOT_DIR + "/c/b/pipe"));
2718+ EXPECT_FALSE(exists(ROOT_DIR + "/c/b/storage-framework-"));
2719+
2720+ // Copy c -> a. This must fail because a exists.
2721+ {
2722+ unique_ptr<ItemJob> job(c.copy(root, "a"));
2723+ wait(job.get());
2724+ ASSERT_EQ(ItemJob::Error, job->status()) << job->error().errorString().toStdString();
2725+ EXPECT_EQ(string("Exists: copy(): \"") + ROOT_DIR + "/a\" exists already",
2726+ job->error().errorString().toStdString());
2727+ }
2728+}
2729+
2730+TEST_F(LocalProviderTest, download)
2731+{
2732+ using namespace unity::storage::qt;
2733+
2734+ set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
2735+
2736+ int const segments = 10000;
2737+ string large_contents;
2738+ large_contents.reserve(file_contents.size() * segments);
2739+ for (int i = 0; i < segments; i++)
2740+ {
2741+ large_contents += file_contents;
2742+ }
2743+ string const full_path = ROOT_DIR + "/foo.txt";
2744+ {
2745+ int fd = open(full_path.c_str(), O_WRONLY | O_CREAT | O_EXCL, 0644);
2746+ ASSERT_GT(fd, 0);
2747+ ASSERT_EQ(ssize_t(large_contents.size()), write(fd, &large_contents[0], large_contents.size())) << strerror(errno);
2748+ ASSERT_EQ(0, close(fd));
2749+ }
2750+
2751+ unique_ptr<ItemJob> job(acc_.get(QString::fromStdString(full_path)));
2752+ wait(job.get());
2753+ EXPECT_TRUE(job->isValid());
2754+
2755+ auto file = job->item();
2756+ unique_ptr<Downloader> downloader(file.createDownloader(Item::ErrorIfConflict));
2757+
2758+ int64_t n_read = 0;
2759+ QObject::connect(downloader.get(), &QIODevice::readyRead,
2760+ [&]() {
2761+ auto bytes = downloader->readAll();
2762+ string const expected = large_contents.substr(n_read, bytes.size());
2763+ EXPECT_EQ(expected, bytes.toStdString());
2764+ n_read += bytes.size();
2765+ });
2766+ QSignalSpy read_finished_spy(downloader.get(), &QIODevice::readChannelFinished);
2767+ ASSERT_TRUE(read_finished_spy.wait(SIGNAL_WAIT_TIME));
2768+
2769+ QSignalSpy status_spy(downloader.get(), &Downloader::statusChanged);
2770+ downloader->close();
2771+ while (downloader->status() == Downloader::Ready)
2772+ {
2773+ ASSERT_TRUE(status_spy.wait(SIGNAL_WAIT_TIME));
2774+ }
2775+ ASSERT_EQ(Downloader::Finished, downloader->status()) << downloader->error().errorString().toStdString();
2776+
2777+ EXPECT_EQ(int64_t(large_contents.size()), n_read);
2778+}
2779+
2780+TEST_F(LocalProviderTest, download_short_read)
2781+{
2782+ using namespace unity::storage::qt;
2783+
2784+ set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
2785+
2786+ int const segments = 10000;
2787+ string const full_path = ROOT_DIR + "/foo.txt";
2788+ {
2789+ int fd = open(full_path.c_str(), O_WRONLY | O_CREAT | O_EXCL, 0644);
2790+ ASSERT_GT(fd, 0);
2791+ for (int i = 0; i < segments; i++)
2792+ {
2793+ ASSERT_EQ(ssize_t(file_contents.size()), write(fd, &file_contents[0], file_contents.size())) << strerror(errno);
2794+ }
2795+ ASSERT_EQ(0, close(fd));
2796+ }
2797+
2798+ unique_ptr<ItemJob> job(acc_.get(QString::fromStdString(full_path)));
2799+ wait(job.get());
2800+ ASSERT_EQ(ItemJob::Finished, job->status()) << job->error().errorString().toStdString();
2801+
2802+ auto file = job->item();
2803+ unique_ptr<Downloader> downloader(file.createDownloader(Item::ErrorIfConflict));
2804+
2805+ QSignalSpy spy(downloader.get(), &Downloader::statusChanged);
2806+ while (downloader->status() == Downloader::Loading)
2807+ {
2808+ ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME));
2809+ }
2810+
2811+ downloader->close();
2812+ while (downloader->status() == Downloader::Ready)
2813+ {
2814+ ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME));
2815+ }
2816+ ASSERT_EQ(Downloader::Error, downloader->status()) << downloader->error().errorString().toStdString();
2817+
2818+ auto error = downloader->error();
2819+ EXPECT_EQ(qt::StorageError::LogicError, error.type());
2820+ cout << error.message().toStdString() << endl;
2821+ EXPECT_TRUE(boost::starts_with(error.message().toStdString(),
2822+ "finish() method called too early, file \""
2823+ + full_path + "\" has size 4460000 but only"));
2824+}
2825+
2826+TEST_F(LocalProviderTest, download_etag_mismatch)
2827+{
2828+ using namespace unity::storage::qt;
2829+
2830+ set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
2831+
2832+ string const full_path = ROOT_DIR + "/foo.txt";
2833+ string cmd = string("echo hello >") + full_path;
2834+ ASSERT_EQ(0, system(cmd.c_str()));
2835+
2836+ unique_ptr<ItemJob> job(acc_.get(QString::fromStdString(full_path)));
2837+ wait(job.get());
2838+ ASSERT_EQ(ItemJob::Finished, job->status()) << job->error().errorString().toStdString();
2839+
2840+ auto file = job->item();
2841+
2842+ sleep(1);
2843+ cmd = string("touch ") + full_path;
2844+ ASSERT_EQ(0, system(cmd.c_str()));
2845+
2846+ unique_ptr<Downloader> downloader(file.createDownloader(Item::ErrorIfConflict));
2847+
2848+ QSignalSpy spy(downloader.get(), &Downloader::statusChanged);
2849+ while (downloader->status() != Downloader::Error)
2850+ {
2851+ ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME));
2852+ }
2853+
2854+ auto error = downloader->error();
2855+ EXPECT_EQ(qt::StorageError::Conflict, error.type());
2856+ EXPECT_EQ("download(): etag mismatch", error.message().toStdString());
2857+}
2858+
2859+TEST_F(LocalProviderTest, download_wrong_file_type)
2860+{
2861+ // We can't try a download for a directory via the client API, so we use the LocalDownloadJob directly.
2862+
2863+ auto p = make_shared<LocalProvider>();
2864+
2865+ string const dir = ROOT_DIR + "/dir";
2866+ ASSERT_EQ(0, mkdir(dir.c_str(), 0755));
2867+
2868+ try
2869+ {
2870+ LocalDownloadJob(p, dir, "some_etag");
2871+ FAIL();
2872+ }
2873+ catch (provider::InvalidArgumentException const& e)
2874+ {
2875+ EXPECT_EQ(string("InvalidArgumentException: download(): \"" + dir + "\" is not a file"), e.what());
2876+ }
2877+}
2878+
2879+TEST_F(LocalProviderTest, download_no_permission)
2880+{
2881+ using namespace unity::storage::qt;
2882+
2883+ set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
2884+
2885+ string const full_path = ROOT_DIR + "/foo.txt";
2886+ string cmd = string("echo hello >") + full_path;
2887+ ASSERT_EQ(0, system(cmd.c_str()));
2888+
2889+ unique_ptr<ItemJob> job(acc_.get(QString::fromStdString(full_path)));
2890+ wait(job.get());
2891+ ASSERT_EQ(ItemJob::Finished, job->status()) << job->error().errorString().toStdString();
2892+
2893+ auto file = job->item();
2894+
2895+ ASSERT_EQ(0, chmod(full_path.c_str(), 0244));
2896+
2897+ unique_ptr<Downloader> downloader(file.createDownloader(Item::ErrorIfConflict));
2898+
2899+ QSignalSpy spy(downloader.get(), &Downloader::statusChanged);
2900+ while (downloader->status() != Downloader::Error)
2901+ {
2902+ ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME));
2903+ }
2904+
2905+ auto error = downloader->error();
2906+ EXPECT_EQ(qt::StorageError::ResourceError, error.type());
2907+ EXPECT_EQ(string("download(): : cannot open \"") + full_path + "\": Permission denied (QFileDevice::FileError = 5)",
2908+ error.message().toStdString());
2909+}
2910+
2911+TEST_F(LocalProviderTest, download_no_such_file)
2912+{
2913+ // We can't try a download for a non-existent file via the client API, so we use the LocalDownloadJob directly.
2914+
2915+ auto p = make_shared<LocalProvider>();
2916+
2917+ try
2918+ {
2919+ LocalDownloadJob(p, ROOT_DIR + "/no_such_file", "some_etag");
2920+ FAIL();
2921+ }
2922+ catch (provider::NotExistsException const&)
2923+ {
2924+ }
2925+}
2926+
2927+TEST_F(LocalProviderTest, update)
2928+{
2929+ using namespace unity::storage::qt;
2930+
2931+ set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
2932+
2933+ auto full_path = ROOT_DIR + "/foo.txt";
2934+ auto cmd = string("echo hello >") + full_path;
2935+ ASSERT_EQ(0, system(cmd.c_str()));
2936+
2937+ unique_ptr<ItemJob> job(acc_.get(QString::fromStdString(full_path)));
2938+ wait(job.get());
2939+ EXPECT_TRUE(job->isValid());
2940+
2941+ auto file = job->item();
2942+ auto old_etag = file.etag();
2943+
2944+ int const segments = 50;
2945+ unique_ptr<Uploader> uploader(file.createUploader(Item::ErrorIfConflict, file_contents.size() * segments));
2946+
2947+ int count = 0;
2948+ QTimer timer;
2949+ timer.setSingleShot(false);
2950+ timer.setInterval(10);
2951+ QObject::connect(&timer, &QTimer::timeout, [&] {
2952+ uploader->write(&file_contents[0], file_contents.size());
2953+ count++;
2954+ if (count == segments)
2955+ {
2956+ uploader->close();
2957+ }
2958+ });
2959+
2960+ QSignalSpy spy(uploader.get(), &Uploader::statusChanged);
2961+ timer.start();
2962+ while (uploader->status() == Uploader::Loading ||
2963+ uploader->status() == Uploader::Ready)
2964+ {
2965+ ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME));
2966+ }
2967+ ASSERT_EQ(Uploader::Finished, uploader->status()) << uploader->error().errorString().toStdString();
2968+
2969+ file = uploader->item();
2970+ EXPECT_NE(old_etag, file.etag());
2971+ EXPECT_EQ(int64_t(file_contents.size() * segments), file.sizeInBytes());
2972+}
2973+
2974+TEST_F(LocalProviderTest, update_empty)
2975+{
2976+ using namespace unity::storage::qt;
2977+
2978+ set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
2979+
2980+ auto full_path = ROOT_DIR + "/foo.txt";
2981+ auto cmd = string("echo hello >") + full_path;
2982+ ASSERT_EQ(0, system(cmd.c_str()));
2983+
2984+ unique_ptr<ItemJob> job(acc_.get(QString::fromStdString(full_path)));
2985+ wait(job.get());
2986+ EXPECT_TRUE(job->isValid());
2987+
2988+ auto file = job->item();
2989+ auto old_etag = file.etag();
2990+
2991+ sleep(1); // Make sure mtime changes.
2992+ unique_ptr<Uploader> uploader(file.createUploader(Item::ErrorIfConflict, 0));
2993+ {
2994+ QSignalSpy spy(uploader.get(), &Uploader::statusChanged);
2995+ while (uploader->status() != Uploader::Ready)
2996+ {
2997+ ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME));
2998+ }
2999+ }
3000+
3001+ QSignalSpy spy(uploader.get(), &Uploader::statusChanged);
3002+ uploader->close();
3003+ while (uploader->status() != Uploader::Finished)
3004+ {
3005+ ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME));
3006+ }
3007+
3008+ file = uploader->item();
3009+ EXPECT_NE(old_etag, file.etag());
3010+ EXPECT_EQ(int64_t(0), file.sizeInBytes());
3011+}
3012+
3013+TEST_F(LocalProviderTest, update_cancel)
3014+{
3015+ using namespace unity::storage::qt;
3016+
3017+ set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
3018+
3019+ auto full_path = ROOT_DIR + "/foo.txt";
3020+ auto cmd = string("echo hello >") + full_path;
3021+ ASSERT_EQ(0, system(cmd.c_str()));
3022+
3023+ unique_ptr<ItemJob> job(acc_.get(QString::fromStdString(full_path)));
3024+ wait(job.get());
3025+ EXPECT_TRUE(job->isValid());
3026+
3027+ auto file = job->item();
3028+ auto old_etag = file.etag();
3029+
3030+ int const segments = 50;
3031+ unique_ptr<Uploader> uploader(file.createUploader(Item::ErrorIfConflict, file_contents.size() * segments));
3032+
3033+ int count = 0;
3034+ QTimer timer;
3035+ timer.setSingleShot(false);
3036+ timer.setInterval(10);
3037+ QObject::connect(&timer, &QTimer::timeout, [&] {
3038+ uploader->write(&file_contents[0], file_contents.size());
3039+ count++;
3040+ if (count == segments / 2)
3041+ {
3042+ uploader->cancel();
3043+ }
3044+ else if (count == segments)
3045+ {
3046+ uploader->close();
3047+ }
3048+ });
3049+
3050+ QSignalSpy spy(uploader.get(), &Uploader::statusChanged);
3051+ timer.start();
3052+ while (uploader->status() != Uploader::Cancelled)
3053+ {
3054+ ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME));
3055+ }
3056+}
3057+
3058+TEST_F(LocalProviderTest, update_file_touched_before_uploading)
3059+{
3060+ using namespace unity::storage::qt;
3061+
3062+ set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
3063+
3064+ auto full_path = ROOT_DIR + "/foo.txt";
3065+ auto cmd = string("echo hello >") + full_path;
3066+ ASSERT_EQ(0, system(cmd.c_str()));
3067+
3068+ unique_ptr<ItemJob> job(acc_.get(QString::fromStdString(full_path)));
3069+ wait(job.get());
3070+ EXPECT_TRUE(job->isValid());
3071+
3072+ auto file = job->item();
3073+ auto old_etag = file.etag();
3074+
3075+ sleep(1); // Make sure mtime changes.
3076+ cmd = string("touch ") + full_path;
3077+ ASSERT_EQ(0, system(cmd.c_str()));
3078+ unique_ptr<Uploader> uploader(file.createUploader(Item::ErrorIfConflict, 0));
3079+ QSignalSpy spy(uploader.get(), &Uploader::statusChanged);
3080+ while (uploader->status() != Uploader::Error)
3081+ {
3082+ ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME));
3083+ }
3084+ EXPECT_EQ("update(): etag mismatch", uploader->error().message().toStdString());
3085+}
3086+
3087+TEST_F(LocalProviderTest, update_file_touched_while_uploading)
3088+{
3089+ using namespace unity::storage::qt;
3090+
3091+ set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
3092+
3093+ auto full_path = ROOT_DIR + "/foo.txt";
3094+ auto cmd = string("echo hello >") + full_path;
3095+ ASSERT_EQ(0, system(cmd.c_str()));
3096+
3097+ unique_ptr<ItemJob> job(acc_.get(QString::fromStdString(full_path)));
3098+ wait(job.get());
3099+ EXPECT_TRUE(job->isValid());
3100+
3101+ auto file = job->item();
3102+ auto old_etag = file.etag();
3103+
3104+ int const segments = 50;
3105+ unique_ptr<Uploader> uploader(file.createUploader(Item::ErrorIfConflict, file_contents.size() * segments));
3106+
3107+ int count = 0;
3108+ QTimer timer;
3109+ timer.setSingleShot(false);
3110+ timer.setInterval(10);
3111+ QObject::connect(&timer, &QTimer::timeout, [&] {
3112+ uploader->write(&file_contents[0], file_contents.size());
3113+ count++;
3114+ if (count == segments / 2)
3115+ {
3116+ sleep(1);
3117+ cmd = string("touch ") + full_path;
3118+ ASSERT_EQ(0, system(cmd.c_str()));
3119+ }
3120+ else if (count == segments)
3121+ {
3122+ uploader->close();
3123+ }
3124+ });
3125+
3126+ QSignalSpy spy(uploader.get(), &Uploader::statusChanged);
3127+ timer.start();
3128+ while (uploader->status() != Uploader::Error)
3129+ {
3130+ ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME));
3131+ }
3132+ ASSERT_EQ(Uploader::Error, uploader->status());
3133+ EXPECT_EQ("update(): etag mismatch", uploader->error().message().toStdString());
3134+}
3135+
3136+TEST_F(LocalProviderTest, update_ignore_etag_mismatch)
3137+{
3138+ using namespace unity::storage::qt;
3139+
3140+ set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
3141+
3142+ auto full_path = ROOT_DIR + "/foo.txt";
3143+ auto cmd = string("echo hello >") + full_path;
3144+ ASSERT_EQ(0, system(cmd.c_str()));
3145+
3146+ unique_ptr<ItemJob> job(acc_.get(QString::fromStdString(full_path)));
3147+ wait(job.get());
3148+ EXPECT_TRUE(job->isValid());
3149+
3150+ auto file = job->item();
3151+ auto old_etag = file.etag();
3152+
3153+ sleep(1); // Make sure mtime changes.
3154+ cmd = string("touch ") + full_path;
3155+ unique_ptr<Uploader> uploader(file.createUploader(Item::IgnoreConflict, 0));
3156+ {
3157+ QSignalSpy spy(uploader.get(), &Uploader::statusChanged);
3158+ while (uploader->status() != Uploader::Ready)
3159+ {
3160+ ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME));
3161+ }
3162+ }
3163+
3164+ QSignalSpy spy(uploader.get(), &Uploader::statusChanged);
3165+ uploader->close();
3166+ while (uploader->status() != Uploader::Finished)
3167+ {
3168+ ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME));
3169+ }
3170+
3171+ file = uploader->item();
3172+ EXPECT_NE(old_etag, file.etag());
3173+ EXPECT_EQ(int64_t(0), file.sizeInBytes());
3174+}
3175+
3176+TEST_F(LocalProviderTest, update_close_too_soon)
3177+{
3178+ using namespace unity::storage::qt;
3179+
3180+ set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
3181+
3182+ auto full_path = ROOT_DIR + "/foo.txt";
3183+ auto cmd = string("echo hello >") + full_path;
3184+ ASSERT_EQ(0, system(cmd.c_str()));
3185+
3186+ unique_ptr<ItemJob> job(acc_.get(QString::fromStdString(full_path)));
3187+ wait(job.get());
3188+ EXPECT_TRUE(job->isValid());
3189+
3190+ auto file = job->item();
3191+ auto old_etag = file.etag();
3192+
3193+ int const segments = 50;
3194+ unique_ptr<Uploader> uploader(file.createUploader(Item::ErrorIfConflict, file_contents.size() * segments));
3195+
3196+ int count = 0;
3197+ QTimer timer;
3198+ timer.setSingleShot(false);
3199+ timer.setInterval(10);
3200+ QObject::connect(&timer, &QTimer::timeout, [&] {
3201+ uploader->write(&file_contents[0], file_contents.size());
3202+ count++;
3203+ if (count == segments - 1)
3204+ {
3205+ uploader->close();
3206+ }
3207+ });
3208+
3209+ QSignalSpy spy(uploader.get(), &Uploader::statusChanged);
3210+ timer.start();
3211+ while (uploader->status() != Uploader::Error)
3212+ {
3213+ ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME));
3214+ }
3215+ ASSERT_EQ(Uploader::Error, uploader->status()) << uploader->error().errorString().toStdString();
3216+ EXPECT_EQ("LogicError: finish() method called too early, size was given as 22300 but only 21854 bytes were received",
3217+ uploader->error().errorString().toStdString());
3218+}
3219+
3220+TEST_F(LocalProviderTest, update_write_too_much)
3221+{
3222+ using namespace unity::storage::qt;
3223+
3224+ set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
3225+
3226+ auto full_path = ROOT_DIR + "/foo.txt";
3227+ auto cmd = string("echo hello >") + full_path;
3228+ ASSERT_EQ(0, system(cmd.c_str()));
3229+
3230+ unique_ptr<ItemJob> job(acc_.get(QString::fromStdString(full_path)));
3231+ wait(job.get());
3232+ EXPECT_TRUE(job->isValid());
3233+
3234+ auto file = job->item();
3235+ auto old_etag = file.etag();
3236+
3237+ int const segments = 50;
3238+ // We write more than this many bytes below.
3239+ unique_ptr<Uploader> uploader(file.createUploader(Item::ErrorIfConflict, file_contents.size() * segments - 1));
3240+
3241+ int count = 0;
3242+ QTimer timer;
3243+ timer.setSingleShot(false);
3244+ timer.setInterval(10);
3245+ QObject::connect(&timer, &QTimer::timeout, [&] {
3246+ uploader->write(&file_contents[0], file_contents.size());
3247+ count++;
3248+ if (count == segments)
3249+ {
3250+ uploader->close();
3251+ }
3252+ });
3253+
3254+ QSignalSpy spy(uploader.get(), &Uploader::statusChanged);
3255+ timer.start();
3256+ while (uploader->status() != Uploader::Error)
3257+ {
3258+ ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME));
3259+ }
3260+ ASSERT_EQ(Uploader::Error, uploader->status());
3261+ EXPECT_EQ("update(): received more than the expected number (22299) of bytes",
3262+ uploader->error().message().toStdString());
3263+}
3264+
3265+TEST_F(LocalProviderTest, upload_wrong_file_type)
3266+{
3267+ // We can't try an upload for a directory via the client API, so we use the LocalUploadJob directly.
3268+
3269+ auto p = make_shared<LocalProvider>();
3270+
3271+ string const dir = ROOT_DIR + "/dir";
3272+ ASSERT_EQ(0, mkdir(dir.c_str(), 0755));
3273+
3274+ try
3275+ {
3276+ LocalUploadJob(p, dir, 0, "");
3277+ FAIL();
3278+ }
3279+ catch (provider::InvalidArgumentException const& e)
3280+ {
3281+ EXPECT_EQ(string("InvalidArgumentException: update(): \"" + dir + "\" is not a file"), e.what());
3282+ }
3283+}
3284+
3285+TEST_F(LocalProviderTest, upload_root_noperm)
3286+{
3287+ // Force an error in prepare_channels when creating temp file.
3288+
3289+ auto p = make_shared<LocalProvider>();
3290+
3291+ ASSERT_EQ(0, chmod(ROOT_DIR.c_str(), 0644));
3292+
3293+ try
3294+ {
3295+ LocalUploadJob(p, ROOT_DIR, "name", 0, true);
3296+ chmod(ROOT_DIR.c_str(), 0755);
3297+ FAIL();
3298+ }
3299+ catch (provider::ResourceException const& e)
3300+ {
3301+ EXPECT_EQ(string("ResourceException: create_file(): cannot create temp file \"") + ROOT_DIR
3302+ + "/.storage-framework-%%%%-%%%%-%%%%-%%%%\": Invalid argument",
3303+ e.what());
3304+ }
3305+ chmod(ROOT_DIR.c_str(), 0755);
3306+}
3307+
3308+TEST_F(LocalProviderTest, sanitize)
3309+{
3310+ // Force various errors in sanitize() for coverage.
3311+
3312+ auto p = make_shared<LocalProvider>();
3313+
3314+ try
3315+ {
3316+ LocalUploadJob(p, ROOT_DIR, "a/b", 0, true);
3317+ FAIL();
3318+ }
3319+ catch (provider::InvalidArgumentException const& e)
3320+ {
3321+ EXPECT_STREQ("InvalidArgumentException: create_file(): name \"a/b\" cannot contain a slash", e.what());
3322+ }
3323+
3324+ try
3325+ {
3326+ LocalUploadJob(p, ROOT_DIR, "..", 0, true);
3327+ FAIL();
3328+ }
3329+ catch (provider::InvalidArgumentException const& e)
3330+ {
3331+ EXPECT_STREQ("InvalidArgumentException: create_file(): invalid name: \"..\"", e.what());
3332+ }
3333+
3334+ try
3335+ {
3336+ LocalUploadJob(p, ROOT_DIR, ".storage-framework", 0, true);
3337+ FAIL();
3338+ }
3339+ catch (provider::InvalidArgumentException const& e)
3340+ {
3341+ EXPECT_STREQ("InvalidArgumentException: create_file(): names beginning with \".storage-framework\" are reserved",
3342+ e.what());
3343+ }
3344+}
3345+
3346+TEST_F(LocalProviderTest, throw_if_not_valid)
3347+{
3348+ // Make sure that we can't escape the root.
3349+
3350+ auto p = make_shared<LocalProvider>();
3351+
3352+ try
3353+ {
3354+ LocalUploadJob(p, ROOT_DIR + "/..", "a", 0, true);
3355+ FAIL();
3356+ }
3357+ catch (provider::InvalidArgumentException const& e)
3358+ {
3359+ EXPECT_EQ(string("InvalidArgumentException: create_file(): invalid id: \"") + ROOT_DIR + "/..\"", e.what());
3360+ }
3361+
3362+ try
3363+ {
3364+ LocalUploadJob(p, "/bin" , "a", 0, true);
3365+ FAIL();
3366+ }
3367+ catch (provider::InvalidArgumentException const& e)
3368+ {
3369+ EXPECT_STREQ("InvalidArgumentException: create_file(): invalid id: \"/bin\"", e.what());
3370+ }
3371+}
3372+
3373+TEST_F(LocalProviderTest, create_file)
3374+{
3375+ using namespace unity::storage::qt;
3376+
3377+ set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
3378+
3379+ auto root = get_root(acc_);
3380+ int const segments = 50;
3381+ unique_ptr<Uploader> uploader(root.createFile("foo.txt", Item::ErrorIfConflict,
3382+ file_contents.size() * segments, "text/plain"));
3383+
3384+ int count = 0;
3385+ QTimer timer;
3386+ timer.setSingleShot(false);
3387+ timer.setInterval(10);
3388+ QObject::connect(&timer, &QTimer::timeout, [&] {
3389+ uploader->write(&file_contents[0], file_contents.size());
3390+ count++;
3391+ if (count == segments)
3392+ {
3393+ uploader->close();
3394+ }
3395+ });
3396+
3397+ QSignalSpy spy(uploader.get(), &Uploader::statusChanged);
3398+ while (uploader->status() != Uploader::Ready)
3399+ {
3400+ ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME));
3401+ }
3402+ timer.start();
3403+ while (uploader->status() != Uploader::Finished)
3404+ {
3405+ ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME));
3406+ }
3407+
3408+ auto file = uploader->item();
3409+ EXPECT_EQ(int64_t(file_contents.size() * segments), file.sizeInBytes());
3410+}
3411+
3412+TEST_F(LocalProviderTest, create_file_ignore_conflict)
3413+{
3414+ using namespace unity::storage::qt;
3415+
3416+ set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
3417+
3418+ auto full_path = ROOT_DIR + "/foo.txt";
3419+ auto cmd = string("echo hello >") + full_path;
3420+ ASSERT_EQ(0, system(cmd.c_str()));
3421+
3422+ auto root = get_root(acc_);
3423+ int const segments = 50;
3424+ unique_ptr<Uploader> uploader(root.createFile("foo.txt", Item::IgnoreConflict,
3425+ file_contents.size() * segments, "text/plain"));
3426+
3427+ int count = 0;
3428+ QTimer timer;
3429+ timer.setSingleShot(false);
3430+ timer.setInterval(10);
3431+ QObject::connect(&timer, &QTimer::timeout, [&] {
3432+ uploader->write(&file_contents[0], file_contents.size());
3433+ count++;
3434+ if (count == segments)
3435+ {
3436+ uploader->close();
3437+ }
3438+ });
3439+
3440+ QSignalSpy spy(uploader.get(), &Uploader::statusChanged);
3441+ while (uploader->status() != Uploader::Ready)
3442+ {
3443+ ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME));
3444+ }
3445+ timer.start();
3446+ while (uploader->status() != Uploader::Finished)
3447+ {
3448+ ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME));
3449+ }
3450+
3451+ auto file = uploader->item();
3452+ EXPECT_EQ(int64_t(file_contents.size() * segments), file.sizeInBytes());
3453+}
3454+
3455+TEST_F(LocalProviderTest, create_file_error_if_conflict)
3456+{
3457+ using namespace unity::storage::qt;
3458+
3459+ set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
3460+
3461+ auto full_path = ROOT_DIR + "/foo.txt";
3462+ auto cmd = string("echo hello >") + full_path;
3463+ ASSERT_EQ(0, system(cmd.c_str()));
3464+
3465+ auto root = get_root(acc_);
3466+ int const segments = 50;
3467+ unique_ptr<Uploader> uploader(root.createFile("foo.txt", Item::ErrorIfConflict,
3468+ file_contents.size() * segments, "text/plain"));
3469+
3470+ QSignalSpy spy(uploader.get(), &Uploader::statusChanged);
3471+ while (uploader->status() != Uploader::Error)
3472+ {
3473+ ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME));
3474+ }
3475+ EXPECT_EQ(string("create_file(): \"") + full_path + "\" exists already",
3476+ uploader->error().message().toStdString());
3477+}
3478+
3479+TEST_F(LocalProviderTest, create_file_created_during_upload)
3480+{
3481+ using namespace unity::storage::qt;
3482+
3483+ set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
3484+
3485+ auto root = get_root(acc_);
3486+ int const segments = 50;
3487+ string full_path = ROOT_DIR + "/foo.txt";
3488+ unique_ptr<Uploader> uploader(root.createFile("foo.txt", Item::ErrorIfConflict,
3489+ file_contents.size() * segments, "text/plain"));
3490+
3491+ int count = 0;
3492+ QTimer timer;
3493+ timer.setSingleShot(false);
3494+ timer.setInterval(10);
3495+ QObject::connect(&timer, &QTimer::timeout, [&] {
3496+ uploader->write(&file_contents[0], file_contents.size());
3497+ count++;
3498+ if (count == segments / 2)
3499+ {
3500+ string cmd = "touch " + full_path;
3501+ ASSERT_EQ(0, system(cmd.c_str()));
3502+ }
3503+ else if (count == segments)
3504+ {
3505+ uploader->close();
3506+ }
3507+ });
3508+
3509+ QSignalSpy spy(uploader.get(), &Uploader::statusChanged);
3510+ while (uploader->status() != Uploader::Ready)
3511+ {
3512+ ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME));
3513+ }
3514+ timer.start();
3515+ while (uploader->status() != Uploader::Error)
3516+ {
3517+ ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME));
3518+ }
3519+ EXPECT_EQ(string("create_file(): \"") + full_path + "\" exists already",
3520+ uploader->error().message().toStdString());
3521+}
3522+
3523+int main(int argc, char** argv)
3524+{
3525+ setenv("LANG", "C", true);
3526+ setenv("SF_LOCAL_PROVIDER_ROOT", ROOT_DIR.c_str(), true);
3527+
3528+ // Test test fixture repeatedly creates and tears down the dbus connection.
3529+ // The provider calls g_file_new_for_path() which talks to the GVfs backend
3530+ // via dbus. If the dbus connection disappears, that causes GIO to send a
3531+ // a SIGTERM, killing the test.
3532+ // Setting GIO_USE_VFS variable to "local" disables sending the signal.
3533+ setenv("GIO_USE_VFS", "local", true);
3534+
3535+ QCoreApplication app(argc, argv);
3536+
3537+ ::testing::InitGoogleTest(&argc, argv);
3538+ int rc = RUN_ALL_TESTS();
3539+
3540+ // Process any pending events to avoid bogus leak reports from valgrind.
3541+ QCoreApplication::sendPostedEvents();
3542+ QCoreApplication::processEvents();
3543+
3544+ if (rc == 0)
3545+ {
3546+ boost::system::error_code ec;
3547+ boost::filesystem::remove_all(ROOT_DIR, ec);
3548+ }
3549+
3550+ return rc;
3551+}
3552
3553=== added file 'tests/utils/env_var_guard.h'
3554--- tests/utils/env_var_guard.h 1970-01-01 00:00:00 +0000
3555+++ tests/utils/env_var_guard.h 2017-03-17 05:04:57 +0000
3556@@ -0,0 +1,70 @@
3557+/*
3558+ * Copyright (C) 2015 Canonical Ltd.
3559+ *
3560+ * This program is free software: you can redistribute it and/or modify
3561+ * it under the terms of the GNU General Public License version 3 as
3562+ * published by the Free Software Foundation.
3563+ *
3564+ * This program is distributed in the hope that it will be useful,
3565+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
3566+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3567+ * GNU General Public License for more details.
3568+ *
3569+ * You should have received a copy of the GNU General Public License
3570+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
3571+ *
3572+ * Authored by: Michi Henning <michi.henning@canonical.com>
3573+ */
3574+
3575+#pragma once
3576+
3577+#include <cassert>
3578+#include <cstdlib>
3579+#include <string>
3580+
3581+class EnvVarGuard final
3582+{
3583+public:
3584+ // Set environment variable 'name' to 'val'.
3585+ // To clear the variable, pass nullptr for 'val'.
3586+ // The destructor restores the original setting.
3587+ EnvVarGuard(char const* name, char const* val)
3588+ : name_(name)
3589+ {
3590+ assert(name && *name != '\0');
3591+ auto const old_val = getenv(name);
3592+ if ((was_set_ = old_val != nullptr))
3593+ {
3594+ old_value_ = old_val;
3595+ }
3596+ if (val)
3597+ {
3598+ setenv(name, val, true);
3599+ }
3600+ else
3601+ {
3602+ unsetenv(name);
3603+ }
3604+ }
3605+
3606+ // Restore the original setting.
3607+ ~EnvVarGuard()
3608+ {
3609+ if (was_set_)
3610+ {
3611+ setenv(name_.c_str(), old_value_.c_str(), true);
3612+ }
3613+ else
3614+ {
3615+ unsetenv(name_.c_str());
3616+ }
3617+ }
3618+
3619+ EnvVarGuard(const EnvVarGuard&) = delete;
3620+ EnvVarGuard& operator=(const EnvVarGuard&) = delete;
3621+
3622+private:
3623+ std::string name_;
3624+ std::string old_value_;
3625+ bool was_set_;
3626+};

Subscribers

People subscribed via source and target branches

to all changes: