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
=== modified file 'CMakeLists.txt'
--- CMakeLists.txt 2017-02-24 03:01:57 +0000
+++ CMakeLists.txt 2017-03-17 05:04:57 +0000
@@ -43,7 +43,7 @@
43include_directories(include)43include_directories(include)
44include_directories(${CMAKE_BINARY_DIR}/include) # For generated headers44include_directories(${CMAKE_BINARY_DIR}/include) # For generated headers
4545
46set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC --std=c++11 -Wall -pedantic -Wextra -fvisibility=hidden")46set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC --std=c++14 -Wall -pedantic -Wextra -fvisibility=hidden")
4747
48# Some additional warnings not included by the general flags set above.48# Some additional warnings not included by the general flags set above.
49set(EXTRA_C_WARNINGS "-Wcast-align -Wcast-qual -Wformat -Wredundant-decls -Wswitch-default")49set(EXTRA_C_WARNINGS "-Wcast-align -Wcast-qual -Wformat -Wredundant-decls -Wswitch-default")
@@ -123,6 +123,7 @@
123123
124include(FindPkgConfig)124include(FindPkgConfig)
125pkg_check_modules(APPARMOR_DEPS REQUIRED libapparmor)125pkg_check_modules(APPARMOR_DEPS REQUIRED libapparmor)
126pkg_check_modules(GIO_DEPS REQUIRED gio-2.0 gio-unix-2.0)
126pkg_check_modules(GLIB_DEPS REQUIRED glib-2.0)127pkg_check_modules(GLIB_DEPS REQUIRED glib-2.0)
127pkg_check_modules(ONLINEACCOUNTS_DEPS REQUIRED OnlineAccountsQt)128pkg_check_modules(ONLINEACCOUNTS_DEPS REQUIRED OnlineAccountsQt)
128129
@@ -137,6 +138,7 @@
137138
138enable_coverage_report(139enable_coverage_report(
139 TARGETS140 TARGETS
141 local-provider-lib
140 qt-client-lib-common142 qt-client-lib-common
141 storage-framework-common-internal143 storage-framework-common-internal
142 storage-framework-qt-client144 storage-framework-qt-client
@@ -145,6 +147,7 @@
145 sf-provider-objects147 sf-provider-objects
146 storage-framework-provider148 storage-framework-provider
147 storage-framework-registry149 storage-framework-registry
150 storage-provider-local
148 FILTER151 FILTER
149 ${CMAKE_SOURCE_DIR}/tests/*152 ${CMAKE_SOURCE_DIR}/tests/*
150 ${CMAKE_BINARY_DIR}/*153 ${CMAKE_BINARY_DIR}/*
151154
=== modified file 'debian/control.in'
--- debian/control.in 2016-12-12 02:18:21 +0000
+++ debian/control.in 2017-03-17 05:04:57 +0000
@@ -2,7 +2,7 @@
2Section: libs2Section: libs
3Priority: optional3Priority: optional
4Maintainer: Ubuntu Core Developers <ubuntu-devel-discuss@lists.ubuntu.com>4Maintainer: Ubuntu Core Developers <ubuntu-devel-discuss@lists.ubuntu.com>
5Standards-Version: 3.9.65Standards-Version: 3.9.7
6Build-Depends: cmake,6Build-Depends: cmake,
7 cmake-extras (>= 0.10),7 cmake-extras (>= 0.10),
8 debhelper (>= 9),8 debhelper (>= 9),
@@ -58,6 +58,7 @@
58 online-accounts-daemon,58 online-accounts-daemon,
59Description: Registry for the Storage Framework59Description: Registry for the Storage Framework
60 DBus service that provides access to provider account information.60 DBus service that provides access to provider account information.
61 Includes a local storage provider.
6162
62Package: libstorage-framework-qt-client-2-063Package: libstorage-framework-qt-client-2-0
63Architecture: any64Architecture: any
6465
=== modified file 'debian/storage-framework-registry.install'
--- debian/storage-framework-registry.install 2016-11-21 04:37:37 +0000
+++ debian/storage-framework-registry.install 2017-03-17 05:04:57 +0000
@@ -1,2 +1,4 @@
1usr/lib/*/*/storage-framework-registry1usr/lib/*/*/storage-framework-registry
2usr/share/dbus-1/services/com.canonical.StorageFramework.Registry.service2usr/share/dbus-1/services/com.canonical.StorageFramework.Registry.service
3usr/lib/*/*/storage-provider-local
4usr/share/dbus-1/services/com.canonical.StorageFramework.Provider.Local.service
35
=== added file 'include/unity/storage/internal/gobj_memory.h'
--- include/unity/storage/internal/gobj_memory.h 1970-01-01 00:00:00 +0000
+++ include/unity/storage/internal/gobj_memory.h 2017-03-17 05:04:57 +0000
@@ -0,0 +1,212 @@
1/*
2 * Copyright (C) 2013 Canonical Ltd.
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU Lesser General Public License version 3 as
6 * published by the Free Software Foundation.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU Lesser General Public License for more details.
12 *
13 * You should have received a copy of the GNU Lesser General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 *
16 * Authored by: Jussi Pakkanen <jussi.pakkanen@canonical.com>
17 */
18
19#pragma once
20
21#include <stdexcept>
22
23#pragma GCC diagnostic push
24#pragma GCC diagnostic ignored "-Wold-style-cast"
25#pragma GCC diagnostic ignored "-Wcast-qual"
26#include <glib-object.h>
27
28namespace unity
29{
30
31namespace storage
32{
33
34namespace internal
35{
36
37/**
38 * This class is meant for automatically managing the lifetime of C objects derived
39 * from gobject. Its API perfectly mirrors the API of unique_ptr except that you
40 * can't define your own deleter function as it is always g_object_unref.
41 *
42 * API/ABI stability is not guaranteed. If you need to pass the object across an ABI
43 * boundary, pass the plain gobject.
44 *
45 * This is how you would use gobj_ptr 99% of the time:
46 *
47 * gobj_ptr<GSomeType> o(g_some_type_new(...));
48 *
49 * More specifically, the object will decrement the gobject reference count
50 * of the object it points to when it goes out of scope. It will never increment it.
51 * Thus you should only assign to it when already holding a reference. gobj_ptr
52 * will then take ownership of that particular reference.
53 *
54 * Floating gobjects can not be put in this container as they are meant to be put
55 * into native gobject aware containers immediately upon construction. Trying to insert
56 * a floating gobject into a gobj_ptr will throw an invalid_argument exception. To
57 * prevent accidental memory leaks, the floating gobject is unreffed in this case.
58 */
59template <typename T>
60class gobj_ptr final
61{
62private:
63 T* u;
64
65 void validate_float(T* t)
66 {
67 if (t != nullptr && g_object_is_floating(G_OBJECT(t)))
68 {
69 // LCOV_EXCL_START // False negative from gcovr.
70 throw std::invalid_argument("Tried to add a floating gobject into a gobj_ptr.");
71 // LCOV_EXCL_STOP
72 }
73 }
74
75public:
76 typedef T element_type;
77 typedef T* pointer;
78 typedef decltype(g_object_unref) deleter_type;
79
80 constexpr gobj_ptr() noexcept : u(nullptr)
81 {
82 }
83 explicit gobj_ptr(T* t)
84 : u(t)
85 {
86 // What should we do if validate throws? Unreffing unknown objs
87 // is dodgy but not unreffing runs the risk of
88 // memory leaks. Currently unrefs as u is destroyed
89 // when this exception is thrown.
90 validate_float(t);
91 }
92 constexpr gobj_ptr(std::nullptr_t) noexcept : u(nullptr){};
93 gobj_ptr(gobj_ptr&& o) noexcept
94 {
95 u = o.u;
96 o.u = nullptr;
97 }
98 gobj_ptr(const gobj_ptr& o)
99 : u(nullptr)
100 {
101 *this = o;
102 }
103 gobj_ptr& operator=(const gobj_ptr& o)
104 {
105 if (o.u != nullptr)
106 {
107 g_object_ref(o.u);
108 }
109 reset(o.u);
110 return *this;
111 }
112 ~gobj_ptr()
113 {
114 reset();
115 }
116
117 deleter_type& get_deleter() noexcept
118 {
119 return g_object_unref;
120 }
121 deleter_type& get_deleter() const noexcept
122 {
123 return g_object_unref;
124 }
125
126 void swap(gobj_ptr<T>& o) noexcept
127 {
128 T* tmp = u;
129 u = o.u;
130 o.u = tmp;
131 }
132 void reset(pointer p = pointer())
133 {
134 if (u != nullptr)
135 {
136 g_object_unref(G_OBJECT(u));
137 u = nullptr;
138 }
139 // Same throw dilemma as in pointer constructor.
140 u = p;
141 validate_float(p);
142 }
143
144 T* release() noexcept
145 {
146 T* r = u;
147 u = nullptr;
148 return r;
149 }
150 T* get() const noexcept
151 {
152 return u;
153 }
154
155 T& operator*() const
156 {
157 return *u;
158 }
159 T* operator->() const noexcept
160 {
161 return u;
162 }
163 explicit operator bool() const noexcept
164 {
165 return u != nullptr;
166 }
167
168 gobj_ptr& operator=(gobj_ptr&& o) noexcept
169 {
170 reset();
171 u = o.u;
172 o.u = nullptr;
173 return *this;
174 }
175 gobj_ptr& operator=(std::nullptr_t) noexcept
176 {
177 reset();
178 return *this;
179 }
180 bool operator==(const gobj_ptr<T>& o) const noexcept
181 {
182 return u == o.u;
183 }
184 bool operator!=(const gobj_ptr<T>& o) const noexcept
185 {
186 return u != o.u;
187 }
188 bool operator<(const gobj_ptr<T>& o) const noexcept
189 {
190 return u < o.u;
191 }
192 bool operator<=(const gobj_ptr<T>& o) const noexcept
193 {
194 return u <= o.u;
195 }
196 bool operator>(const gobj_ptr<T>& o) const noexcept
197 {
198 return u > o.u;
199 }
200 bool operator>=(const gobj_ptr<T>& o) const noexcept
201 {
202 return u >= o.u;
203 }
204};
205
206} // namespace internal
207
208} // namespace storage
209
210} // namespace unity
211
212#pragma GCC diagnostic pop
0213
=== modified file 'include/unity/storage/provider/Exceptions.h'
--- include/unity/storage/provider/Exceptions.h 2017-01-27 09:28:03 +0000
+++ include/unity/storage/provider/Exceptions.h 2017-03-17 05:04:57 +0000
@@ -95,7 +95,7 @@
95};95};
9696
97/**97/**
98\brief Indicates that an upload detected a version mismatch.98\brief Indicates that an upload or download detected a version mismatch.
99*/99*/
100class UNITY_STORAGE_EXPORT ConflictException : public StorageException100class UNITY_STORAGE_EXPORT ConflictException : public StorageException
101{101{
102102
=== modified file 'src/CMakeLists.txt'
--- src/CMakeLists.txt 2016-11-07 04:49:34 +0000
+++ src/CMakeLists.txt 2017-03-17 05:04:57 +0000
@@ -2,3 +2,4 @@
2add_subdirectory(provider)2add_subdirectory(provider)
3add_subdirectory(qt)3add_subdirectory(qt)
4add_subdirectory(registry)4add_subdirectory(registry)
5add_subdirectory(local-provider)
56
=== added directory 'src/local-provider'
=== added file 'src/local-provider/CMakeLists.txt'
--- src/local-provider/CMakeLists.txt 1970-01-01 00:00:00 +0000
+++ src/local-provider/CMakeLists.txt 2017-03-17 05:04:57 +0000
@@ -0,0 +1,43 @@
1add_definitions(-DBOOST_THREAD_VERSION=4)
2
3add_library(local-provider-lib STATIC
4 LocalDownloadJob.cpp
5 LocalProvider.cpp
6 LocalUploadJob.cpp
7 utils.cpp
8)
9
10target_include_directories(local-provider-lib PRIVATE
11 ${Qt5Network_INCLUDE_DIRS}
12 ${GLIB_DEPS_INCLUDE_DIRS}
13 ${GIO_DEPS_INCLUDE_DIRS}
14)
15
16set_target_properties(local-provider-lib PROPERTIES
17 AUTOMOC TRUE
18 POSITION_INDEPENDENT_CODE TRUE
19)
20
21add_executable(storage-provider-local
22 main.cpp
23)
24
25target_link_libraries(storage-provider-local
26 local-provider-lib
27 storage-framework-provider
28 Qt5::Network
29 ${GLIB_DEPS_LIBRARIES}
30 ${GIO_DEPS_LIBRARIES}
31)
32
33install(
34 TARGETS storage-provider-local
35 RUNTIME DESTINATION ${CMAKE_INSTALL_LIBDIR}/${PROJECT_NAME}
36)
37
38configure_file(com.canonical.StorageFramework.Provider.Local.service.in com.canonical.StorageFramework.Provider.Local.service)
39
40install(
41 FILES ${CMAKE_CURRENT_BINARY_DIR}/com.canonical.StorageFramework.Provider.Local.service
42 DESTINATION ${CMAKE_INSTALL_DATADIR}/dbus-1/services
43)
044
=== added file 'src/local-provider/LocalDownloadJob.cpp'
--- src/local-provider/LocalDownloadJob.cpp 1970-01-01 00:00:00 +0000
+++ src/local-provider/LocalDownloadJob.cpp 2017-03-17 05:04:57 +0000
@@ -0,0 +1,179 @@
1/*
2 * Copyright (C) 2017 Canonical Ltd
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU Lesser General Public License version 3 as
6 * published by the Free Software Foundation.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU Lesser General Public License for more details.
12 *
13 * You should have received a copy of the GNU Lesser General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 *
16 * Authors: Michi Henning <michi.henning@canonical.com>
17 */
18
19#include "LocalDownloadJob.h"
20
21#include "LocalProvider.h"
22#include "utils.h"
23#include <unity/storage/internal/safe_strerror.h>
24#include <unity/storage/provider/Exceptions.h>
25
26using namespace unity::storage::provider;
27using namespace std;
28
29static int next_download_id = 0;
30
31string const method = "download()";
32
33LocalDownloadJob::LocalDownloadJob(shared_ptr<LocalProvider> const& provider,
34 string const& item_id,
35 string const& match_etag)
36 : DownloadJob(to_string(++next_download_id))
37 , provider_(provider)
38 , item_id_(item_id)
39{
40 using namespace boost::filesystem;
41
42 // Sanitize parameters.
43 provider_->throw_if_not_valid(method, item_id_);
44 try
45 {
46 auto st = status(item_id_);
47 if (!is_regular_file(st))
48 {
49 throw InvalidArgumentException(method + ": \"" + item_id_ + "\" is not a file");
50 }
51 }
52 // LCOV_EXCL_START // Too small a window to hit with a test.
53 catch (filesystem_error const& e)
54 {
55 throw_storage_exception(method, e);
56 }
57 // LCOV_EXCL_STOP
58 if (!match_etag.empty())
59 {
60 int64_t mtime = get_mtime_nsecs(method, item_id_);
61 if (to_string(mtime) != match_etag)
62 {
63 throw ConflictException(method + ": etag mismatch");
64 }
65 }
66
67 // Make input file ready.
68 QString filename = QString::fromStdString(item_id);
69 file_.reset(new QFile(filename));
70 if (!file_->open(QIODevice::ReadOnly))
71 {
72 throw_storage_exception(method,
73 ": cannot open \"" + item_id + "\": " + file_->errorString().toStdString(),
74 file_->error());
75 }
76 bytes_to_write_ = file_->size();
77
78 // Make write socket ready.
79 int dup_fd = dup(write_socket());
80 if (dup_fd == -1)
81 {
82 // LCOV_EXCL_START
83 string msg = "LocalDownloadJob(): dup() failed: " + unity::storage::internal::safe_strerror(errno);
84 throw ResourceException(msg, errno);
85 // LCOV_EXCL_STOP
86 }
87 write_socket_.setSocketDescriptor(dup_fd, QLocalSocket::ConnectedState, QIODevice::WriteOnly);
88 connect(&write_socket_, &QIODevice::bytesWritten, this, &LocalDownloadJob::on_bytes_written);
89
90 // Kick off the read-write cycle.
91 QMetaObject::invokeMethod(this, "read_and_write_chunk", Qt::QueuedConnection);
92}
93
94LocalDownloadJob::~LocalDownloadJob() = default;
95
96boost::future<void> LocalDownloadJob::cancel()
97{
98 disconnect(&write_socket_, nullptr, this, nullptr);
99 write_socket_.abort();
100 file_->close();
101 return boost::make_ready_future();
102}
103
104boost::future<void> LocalDownloadJob::finish()
105{
106 if (bytes_to_write_ > 0)
107 {
108 auto file_size = file_->size();
109 auto written = file_size - bytes_to_write_;
110 string msg = "finish() method called too early, file \"" + item_id_ + "\" has size "
111 + to_string(file_size) + " but only " + to_string(written) + " bytes were consumed";
112 cancel();
113 return boost::make_exceptional_future<void>(LogicException(msg));
114 }
115 // LCOV_EXCL_START
116 // Not reachable because we call report_complete() in read_and_write_chunk().
117 return boost::make_ready_future();
118 // LCOV_EXCL_STOP
119}
120
121void LocalDownloadJob::on_bytes_written(qint64 bytes)
122{
123 bytes_to_write_ -= bytes;
124 assert(bytes_to_write_ >= 0);
125 read_and_write_chunk();
126}
127
128void LocalDownloadJob::read_and_write_chunk()
129{
130 static qint64 constexpr READ_SIZE = 64 * 1024;
131
132 if (bytes_to_write_ == 0)
133 {
134 file_->close();
135 write_socket_.close();
136 report_complete();
137 return;
138 }
139
140 QByteArray buf;
141 buf.resize(READ_SIZE);
142 auto bytes_read = file_->read(buf.data(), buf.size());
143 try
144 {
145 if (bytes_read == -1)
146 {
147 // LCOV_EXCL_START
148 string msg = string("\"") + item_id_ + "\": read error: " + file_->errorString().toStdString();
149 throw_storage_exception(method, msg, file_->error());
150 // LCOV_EXCL_STOP
151 }
152 buf.resize(bytes_read);
153
154 auto bytes_written = write_socket_.write(buf);
155 if (bytes_written == -1)
156 {
157 // LCOV_EXCL_START
158 string msg = string("\"") + item_id_ + "\": socket error: " + write_socket_.errorString().toStdString();
159 throw_storage_exception(method, msg, write_socket_.error());
160 // LCOV_EXCL_STOP
161 }
162 else if (bytes_written != bytes_read)
163 {
164 // LCOV_EXCL_START
165 string msg = string("\"") + item_id_ + "\": socket write error, requested " + to_string(bytes_read)
166 + " B, but wrote only " + to_string(bytes_written) + " B.";
167 throw_storage_exception(method, msg, QLocalSocket::UnknownSocketError);
168 // LCOV_EXCL_STOP
169 }
170 }
171 // LCOV_EXCL_START
172 catch (std::exception const&)
173 {
174 write_socket_.abort();
175 file_->close();
176 report_error(current_exception());
177 }
178 // LCOV_EXCL_STOP
179}
0180
=== added file 'src/local-provider/LocalDownloadJob.h'
--- src/local-provider/LocalDownloadJob.h 1970-01-01 00:00:00 +0000
+++ src/local-provider/LocalDownloadJob.h 2017-03-17 05:04:57 +0000
@@ -0,0 +1,54 @@
1/*
2 * Copyright (C) 2017 Canonical Ltd
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU Lesser General Public License version 3 as
6 * published by the Free Software Foundation.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU Lesser General Public License for more details.
12 *
13 * You should have received a copy of the GNU Lesser General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 *
16 * Authors: Michi Henning <michi.henning@canonical.com>
17 */
18
19#pragma once
20
21#include <unity/storage/provider/DownloadJob.h>
22
23#pragma GCC diagnostic push
24#pragma GCC diagnostic ignored "-Wctor-dtor-privacy"
25#pragma GCC diagnostic ignored "-Wswitch-default"
26#include <QFile>
27#include <QLocalSocket>
28#pragma GCC diagnostic pop
29
30class LocalProvider;
31
32class LocalDownloadJob : public QObject, public unity::storage::provider::DownloadJob
33{
34 Q_OBJECT
35public:
36 LocalDownloadJob(std::shared_ptr<LocalProvider> const& provider,
37 std::string const& item_id,
38 std::string const& match_etag);
39 virtual ~LocalDownloadJob();
40
41 virtual boost::future<void> cancel() override;
42 virtual boost::future<void> finish() override;
43
44private Q_SLOTS:
45 void on_bytes_written(qint64 bytes);
46 void read_and_write_chunk();
47
48private:
49 std::shared_ptr<LocalProvider> const provider_;
50 std::string const item_id_;
51 std::unique_ptr<QFile> file_;
52 QLocalSocket write_socket_;
53 int64_t bytes_to_write_;
54};
055
=== added file 'src/local-provider/LocalProvider.cpp'
--- src/local-provider/LocalProvider.cpp 1970-01-01 00:00:00 +0000
+++ src/local-provider/LocalProvider.cpp 2017-03-17 05:04:57 +0000
@@ -0,0 +1,586 @@
1/*
2 * Copyright (C) 2017 Canonical Ltd
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU Lesser General Public License version 3 as
6 * published by the Free Software Foundation.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU Lesser General Public License for more details.
12 *
13 * You should have received a copy of the GNU Lesser General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 *
16 * Authors: Michi Henning <michi.henning@canonical.com>
17 */
18
19#include "LocalProvider.h"
20
21#include "LocalDownloadJob.h"
22#include "LocalUploadJob.h"
23#include "utils.h"
24
25#include <unity/storage/internal/gobj_memory.h>
26#include <unity/storage/provider/Exceptions.h>
27
28#include <boost/algorithm/string.hpp>
29
30#pragma GCC diagnostic push
31#pragma GCC diagnostic ignored "-Wold-style-cast"
32#include <gio/gio.h>
33#include <glib.h>
34#pragma GCC diagnostic pop
35
36using namespace unity::storage::provider;
37using namespace std;
38
39namespace
40{
41
42// Return the root directory where we store files.
43// If SF_LOCAL_PROVIDER_ROOT is set (used for testing), any files are created
44// directly under the root. E.g., if we do root.createFile("foo.txt", ...), the file
45// will be created as ${SF_LOCAL_PROVIDER_ROOT/foo.txt. SF_LOCAL_PROVIDER_ROOT must
46// be a pre-existing directory.
47//
48// Otherwise, the root is determined by SNAP_USER_COMMON or, if that is not set,
49// by XDG_DATA_HOME. Either way, files are created in a storage-framework/local
50// subdirectory. E.g., if SNAP_USER_COMMON or XDG_DATA_HOME is set to "/tmp" and
51// we do root.createFile("foo.txt", ...), the file will be created as
52// /tmp/storage-framework/local/foo.txt. If /tmp/storage-framework/local does not exist,
53// the directory will be created.
54
55string get_root_dir(string const& method)
56{
57 using namespace boost::filesystem;
58
59 char const* dir = getenv("SF_LOCAL_PROVIDER_ROOT");
60 if (dir && *dir != '\0')
61 {
62 boost::system::error_code ec;
63 if (!exists(dir, ec) || !is_directory(dir, ec))
64 {
65 string msg = method + ": Environment variable SF_LOCAL_PROVIDER_ROOT must denote an existing directory";
66 throw InvalidArgumentException(msg);
67 }
68 return dir;
69 }
70
71 string data_dir;
72 dir = getenv("SNAP_USER_COMMON");
73 if (dir && *dir != '\0')
74 {
75 data_dir = dir;
76 }
77 else
78 {
79 data_dir = g_get_user_data_dir(); // Never fails.
80 }
81 data_dir += "/storage-framework/local";
82
83 try
84 {
85 create_directories(data_dir);
86 }
87 catch (filesystem_error const& e)
88 {
89 throw_storage_exception(method, e);
90 }
91
92 return data_dir;
93}
94
95// Copy a file or directory (recursively). Ignore anything that has the temp file prefix
96// or is not a file or directory.
97
98void copy_recursively(boost::filesystem::path const& source, boost::filesystem::path const& target)
99{
100 using namespace boost::filesystem;
101
102 if (is_reserved_path(source))
103 {
104 return; // Don't copy temporary directories.
105 }
106
107 auto s = status(source);
108 if (is_regular_file(s))
109 {
110 copy_file(source, target);
111 }
112 else if (is_directory(s))
113 {
114 copy_directory(source, target); // Poorly named in boost; this creates the target dir without recursion
115 for (directory_iterator it(source); it != directory_iterator(); ++it)
116 {
117 path source_entry = it->path();
118 path target_entry = target;
119 target_entry /= source_entry.filename();
120 copy_recursively(source_entry, target_entry);
121 }
122 }
123 else
124 {
125 // Ignore everything that's not a directory or file.
126 }
127}
128
129// Convert nanoseconds since the epoch into ISO 8601 date-time.
130
131string make_iso_date(int64_t nsecs_since_epoch)
132{
133 static char const* const FMT = "%Y-%m-%dT%TZ"; // ISO 8601, no fractional seconds.
134
135 struct tm time;
136 time_t secs_since_epoch = nsecs_since_epoch / 1000000000;
137 gmtime_r(&secs_since_epoch, &time);
138
139 char buf[128];
140 strftime(buf, sizeof(buf), FMT, &time);
141 return buf;
142}
143
144string get_content_type(string const& filename)
145{
146 using namespace unity::storage::internal;
147
148 static string const unknown_content_type = "application/octet-stream";
149
150 gobj_ptr<GFile> file(g_file_new_for_path(filename.c_str()));
151 assert(file); // Cannot fail according to doc.
152
153 GError* err = nullptr;
154 gobj_ptr<GFileInfo> full_info(g_file_query_info(file.get(),
155 G_FILE_ATTRIBUTE_STANDARD_FAST_CONTENT_TYPE,
156 G_FILE_QUERY_INFO_NONE,
157 /* cancellable */ NULL,
158 &err));
159 if (!full_info)
160 {
161 return unknown_content_type; // LCOV_EXCL_LINE
162 }
163
164 string content_type = g_file_info_get_attribute_string(full_info.get(), G_FILE_ATTRIBUTE_STANDARD_FAST_CONTENT_TYPE);
165 if (content_type.empty())
166 {
167 return unknown_content_type; // LCOV_EXCL_LINE
168 }
169 return content_type;
170}
171
172// Simple wrapper template that deals with exception handling so we don't
173// have to repeat ourselves endlessly in the various lambdas below.
174// The auto deduction of the return type requires C++ 14.
175
176template<typename F>
177auto invoke_async(string const& method, F& functor)
178{
179 auto lambda = [method, functor]
180 {
181 try
182 {
183 return functor();
184 }
185 catch (StorageException const&)
186 {
187 throw;
188 }
189 catch (boost::filesystem::filesystem_error const& e)
190 {
191 throw_storage_exception(method, e);
192 }
193 // LCOV_EXCL_START
194 catch (std::exception const& e)
195 {
196 throw boost::enable_current_exception(UnknownException(e.what()));
197 }
198 // LCOV_EXCL_STOP
199 };
200 // TODO: boost::async is potentially expensive for some operations. Consider boost::asio thread pool?
201 return boost::async(boost::launch::async, lambda);
202}
203
204} // namespace
205
206LocalProvider::LocalProvider()
207 : root_(boost::filesystem::canonical(get_root_dir("LocalProvider()")))
208{
209}
210
211LocalProvider::~LocalProvider() = default;
212
213boost::future<ItemList> LocalProvider::roots(vector<string> const& /* keys */, Context const& /* context */)
214{
215 vector<Item> roots{ make_item("roots()", root_, status(root_)) };
216 return boost::make_ready_future(roots);
217}
218
219boost::future<tuple<ItemList, string>> LocalProvider::list(string const& item_id,
220 string const& page_token,
221 vector<string> const& /* keys */,
222 Context const& /* context */)
223{
224 string const method = "list()";
225
226 auto This = dynamic_pointer_cast<LocalProvider>(shared_from_this());
227 auto do_list = [This, method, item_id, page_token]
228 {
229 using namespace boost::filesystem;
230
231 This->throw_if_not_valid(method, item_id);
232 vector<Item> items;
233 for (directory_iterator it(item_id); it != directory_iterator(); ++it)
234 {
235 auto dirent = *it;
236 auto path = dirent.path();
237 if (is_reserved_path(path))
238 {
239 continue; // Hide temp files that we create during copy() and move().
240 }
241 Item i;
242 try
243 {
244 auto st = dirent.status();
245 i = This->make_item(method, path, st);
246 items.push_back(i);
247 }
248 catch (std::exception const&)
249 {
250 // We ignore weird errors (such as entries that are not files or folders).
251 }
252 }
253 return tuple<ItemList, string>(items, "");
254 };
255
256 return invoke_async(method, do_list);
257}
258
259boost::future<ItemList> LocalProvider::lookup(string const& parent_id,
260 string const& name,
261 vector<string> const& /* keys */,
262 Context const& /* context */)
263{
264 string const method = "lookup()";
265
266 auto This = dynamic_pointer_cast<LocalProvider>(shared_from_this());
267 auto do_lookup = [This, method, parent_id, name]
268 {
269 using namespace boost::filesystem;
270
271 This->throw_if_not_valid(method, parent_id);
272 auto sanitized_name = sanitize(method, name);
273 path p = parent_id;
274 p /= sanitized_name;
275 This->throw_if_not_valid(method, p.native());
276 auto st = status(p);
277 return vector<Item>{ This->make_item(method, p, st) };
278 };
279
280 return invoke_async(method, do_lookup);
281}
282
283boost::future<Item> LocalProvider::metadata(string const& item_id,
284 vector<string> const& /* keys */,
285 Context const& /* context */)
286{
287 string const method = "metadata()";
288
289 auto This = dynamic_pointer_cast<LocalProvider>(shared_from_this());
290 auto do_metadata = [This, method, item_id]
291 {
292 using namespace boost::filesystem;
293
294 This->throw_if_not_valid(method, item_id);
295 path p = item_id;
296 auto st = status(p);
297 return This->make_item(method, p, st);
298 };
299
300 return invoke_async(method, do_metadata);
301}
302
303boost::future<Item> LocalProvider::create_folder(string const& parent_id,
304 string const& name,
305 vector<string> const& /* keys */,
306 Context const& /* context */)
307{
308 string const method = "create_folder()";
309
310 auto This = dynamic_pointer_cast<LocalProvider>(shared_from_this());
311 auto do_create = [This, method, parent_id, name]
312 {
313 using namespace boost::filesystem;
314
315 This->throw_if_not_valid(method, parent_id);
316 auto sanitized_name = sanitize(method, name);
317 path p = parent_id;
318 p /= sanitized_name;
319 // create_directory() succeeds if the directory exists already, so we need to check explicitly.
320 if (exists(p))
321 {
322 string msg = method + ": \"" + p.native() + "\" exists already";
323 throw boost::enable_current_exception(ExistsException(msg, p.native(), name));
324 }
325 create_directory(p);
326 auto st = status(p);
327 return This->make_item(method, p, st);
328 };
329
330 return invoke_async(method, do_create);
331}
332
333boost::future<unique_ptr<UploadJob>> LocalProvider::create_file(string const& parent_id,
334 string const& name,
335 int64_t size,
336 string const& /* content_type */,
337 bool allow_overwrite,
338 vector<string> const& /* keys */,
339 Context const& /* context */)
340{
341 auto This = dynamic_pointer_cast<LocalProvider>(shared_from_this());
342 boost::promise<unique_ptr<UploadJob>> p;
343 p.set_value(make_unique<LocalUploadJob>(This, parent_id, name, size, allow_overwrite));
344 return p.get_future();
345}
346
347boost::future<unique_ptr<UploadJob>> LocalProvider::update(string const& item_id,
348 int64_t size,
349 string const& old_etag,
350 vector<string> const& /* keys */,
351 Context const& /* context */)
352{
353 auto This = dynamic_pointer_cast<LocalProvider>(shared_from_this());
354 boost::promise<unique_ptr<UploadJob>> p;
355 p.set_value(make_unique<LocalUploadJob>(This, item_id, size, old_etag));
356 return p.get_future();
357}
358
359boost::future<unique_ptr<DownloadJob>> LocalProvider::download(string const& item_id,
360 string const& match_etag,
361 Context const& /* context */)
362{
363 auto This = dynamic_pointer_cast<LocalProvider>(shared_from_this());
364 boost::promise<unique_ptr<DownloadJob>> p;
365 p.set_value(make_unique<LocalDownloadJob>(This, item_id, match_etag));
366 return p.get_future();
367}
368
369boost::future<void> LocalProvider::delete_item(string const& item_id, Context const& /* context */)
370{
371 string const method = "delete_item()";
372
373 auto This = dynamic_pointer_cast<LocalProvider>(shared_from_this());
374 auto do_delete = [This, method, item_id]
375 {
376 using namespace boost::filesystem;
377
378 This->throw_if_not_valid(method, item_id);
379 if (canonical(item_id).native() == This->root_)
380 {
381 string msg = method + ": cannot delete root";
382 throw boost::enable_current_exception(LogicException(msg));
383 }
384 remove_all(item_id);
385 };
386
387 return invoke_async(method, do_delete);
388}
389
390boost::future<Item> LocalProvider::move(string const& item_id,
391 string const& new_parent_id,
392 string const& new_name,
393 vector<string> const& /* keys */,
394 Context const& /* context */)
395{
396 string const method = "move()";
397
398 auto This = dynamic_pointer_cast<LocalProvider>(shared_from_this());
399 auto do_move = [This, method, item_id, new_parent_id, new_name]
400 {
401 using namespace boost::filesystem;
402
403 This->throw_if_not_valid(method, item_id);
404 This->throw_if_not_valid(method, new_parent_id);
405 auto sanitized_name = sanitize(method, new_name);
406
407 path parent_path = new_parent_id;
408 path target_path = parent_path / sanitized_name;
409
410 if (exists(target_path))
411 {
412 string msg = method + ": \"" + target_path.native() + "\" exists already";
413 throw boost::enable_current_exception(ExistsException(msg, target_path.native(), new_name));
414 }
415
416 // Small race condition here: if exists() just said that the target does not exist, it is
417 // possible for it to have been created since. If so, if the target is a file or an empty
418 // directory, it will be removed. In practice, this is unlikely to happen and, if it does,
419 // it is not the end of the world.
420 // TODO: deal with EXDEV
421 rename(item_id, target_path);
422 auto st = status(target_path);
423 return This->make_item(method, target_path, st);
424 };
425
426 return invoke_async(method, do_move);
427}
428
429boost::future<Item> LocalProvider::copy(string const& item_id,
430 string const& new_parent_id,
431 string const& new_name,
432 vector<string> const& /* keys */,
433 Context const& /* context */)
434{
435 string const method = "copy()";
436
437 auto This = dynamic_pointer_cast<LocalProvider>(shared_from_this());
438 auto do_copy = [This, method, item_id, new_parent_id, new_name]
439 {
440 using namespace boost::filesystem;
441
442 This->throw_if_not_valid(method, item_id);
443 This->throw_if_not_valid(method, new_parent_id);
444 auto sanitized_name = sanitize(method, new_name);
445
446 path parent_path = new_parent_id;
447 path target_path = parent_path / sanitized_name;
448
449 if (is_directory(item_id))
450 {
451 if (exists(target_path))
452 {
453 string msg = method + ": \"" + target_path.native() + "\" exists already";
454 throw boost::enable_current_exception(ExistsException(msg, target_path.native(), new_name));
455 }
456
457 // For recursive copy, we create a temporary directory in lieu of target_path and recursively copy
458 // everything into the temporary directory. This ensures that we don't invalidate directory iterators
459 // by creating things while we are iterating, potentially getting trapped in an infinite loop.
460 path tmp_path = canonical(parent_path);
461 tmp_path /= unique_path(string(TMPFILE_PREFIX) + "-%%%%-%%%%-%%%%-%%%%");
462 create_directories(tmp_path);
463 for (directory_iterator it(item_id); it != directory_iterator(); ++it)
464 {
465 if (is_reserved_path(it->path()))
466 {
467 continue; // Don't recurse into the temporary directory
468 }
469 file_status s = it->status();
470 if (is_directory(s) || is_regular_file(s))
471 {
472 path source_entry = it->path();
473 path target_entry = tmp_path;
474 target_entry /= source_entry.filename();
475 copy_recursively(source_entry, target_entry);
476 }
477 }
478 rename(tmp_path, target_path);
479 }
480 else
481 {
482 copy_file(item_id, target_path);
483 }
484
485 auto st = status(target_path);
486 return This->make_item(method, target_path, st);
487 };
488
489 return invoke_async(method, do_copy);
490}
491
492// Make sure that id does not point outside the root.
493
494void LocalProvider::throw_if_not_valid(string const& method, string const& id) const
495{
496 using namespace boost::filesystem;
497
498 string suspect_id;
499 try
500 {
501 suspect_id = canonical(id).native();
502 }
503 catch (filesystem_error const& e)
504 {
505 throw_storage_exception(method, e);
506 }
507
508 // Disallow things such as <root>/blah/../blah even though they lead to the correct path.
509 if (suspect_id != id)
510 {
511 throw boost::enable_current_exception(InvalidArgumentException(method + ": invalid id: \"" + id + "\""));
512 }
513 // id must denote the root or have the root as a prefix.
514 auto const root_id = root_.native();
515 if (id != root_id && !boost::starts_with(id, root_id + "/"))
516 {
517 throw boost::enable_current_exception(InvalidArgumentException(method + ": invalid id: \"" + id + "\""));
518 }
519}
520
521// Return an Item initialized from item_path and st.
522
523Item LocalProvider::make_item(string const& method,
524 boost::filesystem::path const& item_path,
525 boost::filesystem::file_status const& st) const
526{
527 using namespace unity::storage;
528 using namespace unity::storage::metadata;
529 using namespace boost::filesystem;
530
531 map<string, MetadataValue> meta;
532
533 string const item_id = item_path.native();
534 int64_t const mtime_nsecs = get_mtime_nsecs(method, item_id);
535 string const iso_mtime = make_iso_date(mtime_nsecs);
536
537 ItemType type;
538 string name = item_path.filename().native();
539 vector<string> parents{item_path.parent_path().native()};
540 string etag;
541 switch (st.type())
542 {
543 case regular_file:
544 type = ItemType::file;
545 etag = to_string(mtime_nsecs);
546 meta.insert({SIZE_IN_BYTES, int64_t(file_size(item_path))});
547 break;
548 case directory_file:
549 if (item_path == root_)
550 {
551 name = "/";
552 parents.clear();
553 type = ItemType::root;
554 }
555 else
556 {
557 type = ItemType::folder;
558 }
559 break;
560 default:
561 throw boost::enable_current_exception(
562 NotExistsException(method + ": \"" + item_id + "\" is neither a file nor a folder", item_id));
563 }
564
565
566 auto const info = space(item_path);
567 meta.insert({FREE_SPACE_BYTES, int64_t(info.available)});
568 meta.insert({USED_SPACE_BYTES, int64_t(info.capacity - info.available)});
569
570 meta.insert({LAST_MODIFIED_TIME, iso_mtime});
571 meta.insert({CONTENT_TYPE, get_content_type(item_id)});
572
573 auto perms = st.permissions();
574 bool writable;
575 if (type == ItemType::file)
576 {
577 writable = perms & owner_write;
578 }
579 else
580 {
581 writable = perms & owner_write && perms & owner_exe;
582 }
583 meta.insert({WRITABLE, writable});
584
585 return Item{ item_id, parents, name, etag, type, meta };
586}
0587
=== added file 'src/local-provider/LocalProvider.h'
--- src/local-provider/LocalProvider.h 1970-01-01 00:00:00 +0000
+++ src/local-provider/LocalProvider.h 2017-03-17 05:04:57 +0000
@@ -0,0 +1,84 @@
1/*
2 * Copyright (C) 2017 Canonical Ltd
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU Lesser General Public License version 3 as
6 * published by the Free Software Foundation.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU Lesser General Public License for more details.
12 *
13 * You should have received a copy of the GNU Lesser General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 *
16 * Authors: Michi Henning <michi.henning@canonical.com>
17 */
18
19#pragma once
20
21#include <unity/storage/provider/ProviderBase.h>
22
23#include <boost/filesystem.hpp>
24
25class LocalProvider : public unity::storage::provider::ProviderBase
26{
27public:
28 LocalProvider();
29 virtual ~LocalProvider();
30
31 boost::future<unity::storage::provider::ItemList> roots(
32 std::vector<std::string> const& metadata_keys,
33 unity::storage::provider::Context const& ctx) override;
34 boost::future<std::tuple<unity::storage::provider::ItemList, std::string>> list(
35 std::string const& item_id, std::string const& page_token,
36 std::vector<std::string> const& metadata_keys,
37 unity::storage::provider::Context const& ctx) override;
38 boost::future<unity::storage::provider::ItemList> lookup(
39 std::string const& parent_id, std::string const& name,
40 std::vector<std::string> const& metadata_keys,
41 unity::storage::provider::Context const& ctx) override;
42 boost::future<unity::storage::provider::Item> metadata(
43 std::string const& item_id,
44 std::vector<std::string> const& metadata_keys,
45 unity::storage::provider::Context const& ctx) override;
46 boost::future<unity::storage::provider::Item> create_folder(
47 std::string const& parent_id, std::string const& name,
48 std::vector<std::string> const& metadata_keys,
49 unity::storage::provider::Context const& ctx) override;
50 boost::future<std::unique_ptr<unity::storage::provider::UploadJob>> create_file(
51 std::string const& parent_id, std::string const& name,
52 int64_t size, std::string const& content_type, bool allow_overwrite,
53 std::vector<std::string> const& metadata_keys,
54 unity::storage::provider::Context const& ctx) override;
55 boost::future<std::unique_ptr<unity::storage::provider::UploadJob>> update(
56 std::string const& item_id, int64_t size,
57 std::string const& old_etag,
58 std::vector<std::string> const& metadata_keys,
59 unity::storage::provider::Context const& ctx) override;
60 boost::future<std::unique_ptr<unity::storage::provider::DownloadJob>> download(
61 std::string const& item_id,
62 std::string const& match_etag,
63 unity::storage::provider::Context const& ctx) override;
64 boost::future<void> delete_item(std::string const& item_id,
65 unity::storage::provider::Context const& ctx) override;
66 boost::future<unity::storage::provider::Item> move(
67 std::string const& item_id, std::string const& new_parent_id,
68 std::string const& new_name,
69 std::vector<std::string> const& metadata_keys,
70 unity::storage::provider::Context const& ctx) override;
71 boost::future<unity::storage::provider::Item> copy(
72 std::string const& item_id, std::string const& new_parent_id,
73 std::string const& new_name,
74 std::vector<std::string> const& metadata_keys,
75 unity::storage::provider::Context const& ctx) override;
76
77 void throw_if_not_valid(std::string const& method, std::string const& id) const;
78 unity::storage::provider::Item make_item(std::string const& method,
79 boost::filesystem::path const& item_path,
80 boost::filesystem::file_status const& st) const;
81
82private:
83 boost::filesystem::path const root_;
84};
085
=== added file 'src/local-provider/LocalUploadJob.cpp'
--- src/local-provider/LocalUploadJob.cpp 1970-01-01 00:00:00 +0000
+++ src/local-provider/LocalUploadJob.cpp 2017-03-17 05:04:57 +0000
@@ -0,0 +1,338 @@
1/*
2 * Copyright (C) 2017 Canonical Ltd
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU Lesser General Public License version 3 as
6 * published by the Free Software Foundation.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU Lesser General Public License for more details.
12 *
13 * You should have received a copy of the GNU Lesser General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 *
16 * Authors: Michi Henning <michi.henning@canonical.com>
17 */
18
19#include "LocalUploadJob.h"
20
21#include "LocalProvider.h"
22#include "utils.h"
23#include <unity/storage/internal/safe_strerror.h>
24#include <unity/storage/provider/Exceptions.h>
25#include <unity/storage/provider/ProviderBase.h> // TODO: Should not be needed
26
27#include <fcntl.h>
28
29using namespace unity::storage::provider;
30using namespace std;
31
32static int next_upload_id = 0;
33
34LocalUploadJob::LocalUploadJob(shared_ptr<LocalProvider> const& provider, int64_t size, const string& method)
35 : UploadJob(to_string(++next_upload_id))
36 , provider_(provider)
37 , size_(size)
38 , bytes_to_write_(size)
39 , method_(method)
40 , state_(in_progress)
41 , tmp_fd_([](int fd){ if (fd != -1) ::close(fd); })
42{
43}
44
45LocalUploadJob::LocalUploadJob(shared_ptr<LocalProvider> const& provider,
46 string const& parent_id,
47 string const& name,
48 int64_t size,
49 bool allow_overwrite)
50 : LocalUploadJob(provider, size, "create_file()")
51{
52 using namespace boost::filesystem;
53
54 parent_id_ = parent_id;
55 allow_overwrite_ = allow_overwrite;
56
57 provider_->throw_if_not_valid(method_, parent_id);
58
59 auto sanitized_name = sanitize(method_, name);
60 path p = parent_id;
61 p /= sanitized_name;
62 item_id_ = p.native();
63 if (!allow_overwrite && exists(item_id_))
64 {
65 string msg = method_ + ": \"" + item_id_ + "\" exists already";
66 throw ExistsException(msg, item_id_, sanitized_name.native());
67 }
68
69 prepare_channels();
70}
71
72LocalUploadJob::LocalUploadJob(shared_ptr<LocalProvider> const& provider,
73 string const& item_id,
74 int64_t size,
75 string const& old_etag)
76 : LocalUploadJob(provider, size, "update()")
77{
78 using namespace boost::filesystem;
79
80 item_id_ = item_id;
81 provider_->throw_if_not_valid(method_, item_id);
82 try
83 {
84 auto st = status(item_id);
85 if (!is_regular_file(st))
86 {
87 throw InvalidArgumentException(method_ + ": \"" + item_id + "\" is not a file");
88 }
89 }
90 // LCOV_EXCL_START
91 catch (filesystem_error const& e)
92 {
93 // The call to status could throw if the file is unlinked immediately
94 // after the call to throw_if_not_valid.
95 throw_storage_exception(method_, e);
96 }
97 // LCOV_EXCL_STOP
98 if (!old_etag.empty())
99 {
100 int64_t mtime = get_mtime_nsecs(method_, item_id);
101 if (to_string(mtime) != old_etag)
102 {
103 throw ConflictException(method_ + ": etag mismatch");
104 }
105 }
106 old_etag_ = old_etag;
107
108 prepare_channels();
109}
110
111LocalUploadJob::~LocalUploadJob() = default;
112
113void LocalUploadJob::prepare_channels()
114{
115 using namespace boost::filesystem;
116
117 // Open tmp file for writing.
118 auto parent_path = path(item_id_).parent_path();
119 tmp_fd_.reset(open(parent_path.native().c_str(), O_TMPFILE | O_WRONLY, 0600));
120 if (tmp_fd_.get() == -1)
121 {
122 // Some kernels on the phones don't support O_TMPFILE and return various errno values when this fails.
123 // So, if anything at all goes wrong, we fall back on conventional temp file creation and
124 // produce a hard error if that doesn't work either.
125 // Note that, in this case, the temp file retains its name in the file system. Not nice because,
126 // if this process dies at the wrong moment, we leave the temp file behind.
127 use_linkat_ = false;
128 string tmpfile = parent_path.native() + "/" + TMPFILE_PREFIX + "-%%%%-%%%%-%%%%-%%%%";
129 tmp_fd_.reset(mkstemp(const_cast<char*>(tmpfile.data())));
130 if (tmp_fd_.get() == -1)
131 {
132 string msg = method_ + ": cannot create temp file \"" + tmpfile + "\": "
133 + unity::storage::internal::safe_strerror(errno);
134 throw ResourceException(msg, errno);
135 }
136 // LCOV_EXCL_START
137 file_.reset(new QFile(QString::fromStdString(tmpfile)));
138 file_->open(QIODevice::WriteOnly);
139 // LCOV_EXCL_STOP
140 }
141 else
142 {
143 use_linkat_ = true;
144 file_.reset(new QFile);
145 file_->open(tmp_fd_.get(), QIODevice::WriteOnly, QFileDevice::DontCloseHandle);
146 }
147
148 // Make read socket ready.
149 int dup_fd = dup(read_socket());
150 if (dup_fd == -1)
151 {
152 // LCOV_EXCL_START
153 string msg = method_ + ": dup() failed: " + unity::storage::internal::safe_strerror(errno);
154 throw ResourceException(msg, errno);
155 // LCOV_EXCL_STOP
156 }
157 read_socket_.setSocketDescriptor(dup_fd, QLocalSocket::ConnectedState, QIODevice::ReadOnly);
158 connect(&read_socket_, &QLocalSocket::readyRead, this, &LocalUploadJob::on_bytes_ready);
159 connect(&read_socket_, &QIODevice::readChannelFinished, this, &LocalUploadJob::on_read_channel_finished);
160}
161
162boost::future<void> LocalUploadJob::cancel()
163{
164 if (state_ == in_progress)
165 {
166 abort_upload();
167 }
168 return boost::make_ready_future();
169}
170
171boost::future<Item> LocalUploadJob::finish()
172{
173 on_bytes_ready(); // Read any remaining unread buffered data.
174
175 if (bytes_to_write_ > 0)
176 {
177 string msg = "finish() method called too early, size was given as "
178 + to_string(size_) + " but only "
179 + to_string(size_ - bytes_to_write_) + " bytes were received";
180 return boost::make_exceptional_future<Item>(LogicException(msg));
181 }
182
183 // We are committed to finishing successfully or with an error now.
184 state_ = finished;
185
186 try
187 {
188 // We check again for an etag mismatch or overwrite, in case the file was updated after the upload started.
189 if (!parent_id_.empty())
190 {
191 // create_file()
192 if (!allow_overwrite_ && boost::filesystem::exists(item_id_))
193 {
194 string msg = method_ + ": \"" + item_id_ + "\" exists already";
195 boost::filesystem::path(item_id_).filename().native();
196 BOOST_THROW_EXCEPTION(
197 ExistsException(msg, item_id_, boost::filesystem::path(item_id_).filename().native()));
198 }
199 }
200 else if (!old_etag_.empty())
201 {
202 // update()
203 int64_t mtime = get_mtime_nsecs(method_, item_id_);
204 if (to_string(mtime) != old_etag_)
205 {
206 BOOST_THROW_EXCEPTION(ConflictException(method_ + ": etag mismatch"));
207 }
208 }
209
210 if (!file_->flush()) // Make sure that all buffered data is written.
211 {
212 // LCOV_EXCL_START
213 string msg = "finish(): cannot flush output file: " + file_->errorString().toStdString();
214 throw_storage_exception("finish()", msg, file_->error());
215 // LCOV_EXCL_STOP
216 }
217
218 // Link the anonymous tmp file into the file system.
219 using namespace unity::storage::internal;
220
221 if (use_linkat_)
222 {
223 auto old_path = string("/proc/self/fd/") + std::to_string(tmp_fd_.get());
224 ::unlink(item_id_.c_str()); // linkat() will not remove existing file: http://lwn.net/Articles/559969/
225 if (linkat(-1, old_path.c_str(), tmp_fd_.get(), item_id_.c_str(), AT_SYMLINK_FOLLOW) == -1)
226 {
227 // LCOV_EXCL_START
228 string msg = "finish(): linkat \"" + old_path + "\" to \"" + item_id_ + "\" failed: "
229 + safe_strerror(errno);
230 BOOST_THROW_EXCEPTION(ResourceException(msg, errno));
231 // LCOV_EXCL_STOP
232 }
233 }
234 else
235 {
236 // LCOV_EXCL_START
237 auto old_path = file_->fileName().toStdString();
238 if (rename(old_path.c_str(), item_id_.c_str()) == -1)
239 {
240 string msg = "finish(): rename \"" + old_path + "\" to \"" + item_id_ + "\" failed: "
241 + safe_strerror(errno);
242 BOOST_THROW_EXCEPTION(ResourceException(msg, errno));
243 }
244 // LCOV_EXCL_STOP
245 }
246
247 file_->close();
248 read_socket_.close();
249
250 auto st = boost::filesystem::status(item_id_);
251 return boost::make_ready_future<Item>(provider_->make_item(method_, item_id_, st));
252 }
253 catch (StorageException const&)
254 {
255 return boost::make_exceptional_future<Item>(boost::current_exception());
256 }
257 // LCOV_EXCL_START
258 catch (boost::filesystem::filesystem_error const& e)
259 {
260 try
261 {
262 throw_storage_exception("finish()", e);
263 }
264 catch (StorageException const&)
265 {
266 return boost::make_exceptional_future<Item>(boost::current_exception());
267 }
268 }
269 catch (std::exception const& e)
270 {
271 return boost::make_exceptional_future<Item>(UnknownException(e.what()));
272 }
273 // LCOV_EXCL_STOP
274}
275
276void LocalUploadJob::on_bytes_ready()
277{
278 if (bytes_to_write_ < 0)
279 {
280 return; // LCOV_EXCL_LINE // We received too many bytes earlier.
281 }
282
283 try
284 {
285 auto buf = read_socket_.readAll();
286 if (buf.size() != 0)
287 {
288 bytes_to_write_ -= buf.size();
289 if (bytes_to_write_ < 0)
290 {
291 string msg = method_ + ": received more than the expected number (" + to_string(size_) + ") of bytes";
292 throw LogicException(msg);
293 }
294 auto bytes_written = file_->write(buf);
295 if (bytes_written == -1)
296 {
297 // LCOV_EXCL_START
298 string msg = "write error: " + file_->errorString().toStdString();
299 throw_storage_exception(method_, msg, file_->error());
300 // LCOV_EXCL_STOP
301 }
302 else if (bytes_written != buf.size())
303 {
304 // LCOV_EXCL_START
305 string msg = "write error, requested " + to_string(buf.size()) + " B, but wrote only "
306 + to_string(bytes_written) + " B.";
307 throw_storage_exception(method_, msg, QFileDevice::FatalError);
308 // LCOV_EXCL_STOP
309 }
310 }
311 }
312 catch (std::exception const&)
313 {
314 abort_upload();
315 report_error(current_exception());
316 }
317}
318
319void LocalUploadJob::on_read_channel_finished()
320{
321 on_bytes_ready(); // In case there s still buffered data to be read.
322}
323
324void LocalUploadJob::abort_upload()
325{
326 state_ = cancelled;
327 disconnect(&read_socket_, nullptr, this, nullptr);
328 read_socket_.abort();
329 file_->close();
330 if (!use_linkat_)
331 {
332 // LCOV_EXCL_START
333 string filename = file_->fileName().toStdString();
334 ::unlink(filename.c_str()); // Don't leave any temp file behind.
335 // LCOV_EXCL_STOP
336 }
337 bytes_to_write_ = 0;
338}
0339
=== added file 'src/local-provider/LocalUploadJob.h'
--- src/local-provider/LocalUploadJob.h 1970-01-01 00:00:00 +0000
+++ src/local-provider/LocalUploadJob.h 2017-03-17 05:04:57 +0000
@@ -0,0 +1,79 @@
1/*
2 * Copyright (C) 2017 Canonical Ltd
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU Lesser General Public License version 3 as
6 * published by the Free Software Foundation.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU Lesser General Public License for more details.
12 *
13 * You should have received a copy of the GNU Lesser General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 *
16 * Authors: Michi Henning <michi.henning@canonical.com>
17 */
18
19#pragma once
20
21#include <unity/storage/provider/UploadJob.h>
22
23#include <unity/util/ResourcePtr.h>
24
25#pragma GCC diagnostic push
26#pragma GCC diagnostic ignored "-Wctor-dtor-privacy"
27#pragma GCC diagnostic ignored "-Wswitch-default"
28#include <QFile>
29#include <QLocalSocket>
30#pragma GCC diagnostic pop
31
32class LocalProvider;
33
34class LocalUploadJob : public QObject, public unity::storage::provider::UploadJob
35{
36 Q_OBJECT
37public:
38 LocalUploadJob(std::shared_ptr<LocalProvider> const& provider, int64_t size, const std::string& method);
39
40 // create_file()
41 LocalUploadJob(std::shared_ptr<LocalProvider> const& provider,
42 std::string const& parent_id,
43 std::string const& name,
44 int64_t size,
45 bool allow_overwrite);
46 // update()
47 LocalUploadJob(std::shared_ptr<LocalProvider> const& provider,
48 std::string const& item_id,
49 int64_t size,
50 std::string const& old_etag);
51 virtual ~LocalUploadJob();
52
53 virtual boost::future<void> cancel() override;
54 virtual boost::future<unity::storage::provider::Item> finish() override;
55
56private Q_SLOTS:
57 void on_bytes_ready();
58 void on_read_channel_finished();
59
60private:
61 enum State { in_progress, finished, cancelled };
62
63 void prepare_channels();
64 void abort_upload();
65
66 std::shared_ptr<LocalProvider> const provider_;
67 int64_t const size_;
68 int64_t bytes_to_write_;
69 std::unique_ptr<QFile> file_;
70 QLocalSocket read_socket_;
71 std::string const method_;
72 State state_;
73 std::string item_id_;
74 std::string old_etag_; // Empty for create_file()
75 std::string parent_id_; // Empty for update()
76 bool allow_overwrite_; // Undefined for update()
77 unity::util::ResourcePtr<int, std::function<void(int)>> tmp_fd_;
78 bool use_linkat_;
79};
080
=== added file 'src/local-provider/com.canonical.StorageFramework.Provider.Local.service.in'
--- src/local-provider/com.canonical.StorageFramework.Provider.Local.service.in 1970-01-01 00:00:00 +0000
+++ src/local-provider/com.canonical.StorageFramework.Provider.Local.service.in 2017-03-17 05:04:57 +0000
@@ -0,0 +1,3 @@
1[D-BUS Service]
2Name=com.canonical.StorageFramework.Provider.Local
3Exec=@CMAKE_INSTALL_FULL_LIBDIR@/@PROJECT_NAME@/storage-provider-local
04
=== added file 'src/local-provider/main.cpp'
--- src/local-provider/main.cpp 1970-01-01 00:00:00 +0000
+++ src/local-provider/main.cpp 2017-03-17 05:04:57 +0000
@@ -0,0 +1,52 @@
1/*
2 * Copyright (C) 2017 Canonical Ltd
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU Lesser General Public License version 3 as
6 * published by the Free Software Foundation.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU Lesser General Public License for more details.
12 *
13 * You should have received a copy of the GNU Lesser General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 *
16 * Authors: Michi Henning <michi.henning@canonical.com>
17 */
18
19#include <unity/storage/provider/Server.h>
20
21#include "LocalProvider.h"
22
23#include <boost/filesystem.hpp>
24
25#include <iostream>
26
27using namespace std;
28using namespace unity::storage::provider;
29
30int main(int argc, char* argv[])
31{
32 using namespace boost::filesystem;
33
34 string const bus_name = "com.canonical.StorageFramework.Provider.Local";
35 string const account_service_id = "";
36
37 string progname = argv[0];
38
39 try
40 {
41 progname = path(progname).filename().native();
42
43 Server<LocalProvider> server(bus_name, account_service_id);
44 server.init(argc, argv);
45 server.run();
46 }
47 catch (std::exception const& e)
48 {
49 cerr << progname << ": " << e.what() << endl;
50 return 1;
51 }
52}
053
=== added file 'src/local-provider/utils.cpp'
--- src/local-provider/utils.cpp 1970-01-01 00:00:00 +0000
+++ src/local-provider/utils.cpp 2017-03-17 05:04:57 +0000
@@ -0,0 +1,146 @@
1/*
2 * Copyright (C) 2017 Canonical Ltd
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU Lesser General Public License version 3 as
6 * published by the Free Software Foundation.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU Lesser General Public License for more details.
12 *
13 * You should have received a copy of the GNU Lesser General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 *
16 * Authors: Michi Henning <michi.henning@canonical.com>
17 */
18
19#include "utils.h"
20
21#include <unity/storage/internal/safe_strerror.h>
22#include <unity/storage/provider/Exceptions.h>
23
24#include <boost/algorithm/string.hpp>
25#include <boost/exception/enable_current_exception.hpp>
26
27#include <sys/stat.h>
28
29using namespace unity::storage::provider;
30using namespace std;
31
32// Return modification time in nanoseconds since the epoch.
33
34int64_t get_mtime_nsecs(string const& method, string const& path)
35{
36 using namespace unity::storage::internal;
37
38 struct stat st;
39 if (stat(path.c_str(), &st) == -1)
40 {
41 // LCOV_EXCL_START
42 string msg = method + ": cannot stat \"" + path + "\": " + safe_strerror(errno);
43 throw boost::enable_current_exception(ResourceException(msg, errno));
44 // LCOV_EXCL_STOP
45 }
46 return int64_t(st.st_mtim.tv_sec) * 1000000000 + st.st_mtim.tv_nsec;
47}
48
49// Return true if the path uses the temp file prefix.
50
51bool is_reserved_path(boost::filesystem::path const& path)
52{
53 string filename = path.filename().native();
54 return boost::starts_with(filename, TMPFILE_PREFIX);
55}
56
57// Check that name is a valid file or directory name, that is, has a single component
58// and is not "", ".", or "..". Also check that the name does not start with the
59// temp file prefix. Throw if the name is invalid.
60
61boost::filesystem::path sanitize(string const& method, string const& name)
62{
63 using namespace boost::filesystem;
64
65 path p = name;
66 if (!p.parent_path().empty())
67 {
68 // name contains more than one component.
69 string msg = method + ": name \"" + name + "\" cannot contain a slash";
70 throw boost::enable_current_exception(InvalidArgumentException(msg));
71 }
72 path filename = p.filename();
73 if (filename.empty() || filename == "." || filename == "..")
74 {
75 // Not an allowable file name.
76 string msg = method + ": invalid name: \"" + name + "\"";
77 throw boost::enable_current_exception(InvalidArgumentException(msg));
78 }
79 if (is_reserved_path(filename))
80 {
81 string msg = string(method + ": names beginning with \"") + TMPFILE_PREFIX + "\" are reserved";
82 throw boost::enable_current_exception(InvalidArgumentException(msg));
83 }
84 return p;
85}
86
87// Throw a StorageException that corresponds to a boost::filesystem_error.
88
89void throw_storage_exception(string const& method, boost::filesystem::filesystem_error const& e)
90{
91 using namespace boost::system::errc;
92
93 string msg = method + ": ";
94 string path1 = e.path1().native();
95 string path2 = e.path2().native();
96 if (!path2.empty())
97 {
98 msg += "src = \"" + path1 + "\", target = \"" + path2 + "\""; // LCOV_EXCL_LINE
99 }
100 else
101 {
102 msg += "\"" + path1 + "\"";
103 }
104 msg += string(": ") + e.what();
105 switch (e.code().value())
106 {
107 case permission_denied:
108 case operation_not_permitted:
109 throw boost::enable_current_exception(PermissionException(msg));
110 case no_such_file_or_directory:
111 throw boost::enable_current_exception(NotExistsException(msg, e.path1().native()));
112 // LCOV_EXCL_START
113 case file_exists:
114 throw boost::enable_current_exception(
115 ExistsException(msg, e.path1().native(), e.path1().filename().native()));
116 case no_space_on_device:
117 throw boost::enable_current_exception(QuotaException(msg));
118 default:
119 throw boost::enable_current_exception(ResourceException(msg, e.code().value()));
120 // LCOV_EXCL_STOP
121 }
122}
123
124// Throw a storage exception that corresponds to a FileError.
125
126void throw_storage_exception(string const& method, string const& msg, QFileDevice::FileError e)
127{
128 string const error_msg = method + ": " + msg;
129 switch (e)
130 {
131 case QFileDevice::NoError:
132 abort(); // LCOV_EXCL_LINE // Precondition violation
133 break;
134 default:
135 throw ResourceException(error_msg + " (QFileDevice::FileError = " + to_string(e) + ")", e);
136 }
137}
138
139// Throw a storage exception that corresponds to a LocalSocketError.
140
141// LCOV_EXCL_START
142void throw_storage_exception(string const& method, string const& msg, QLocalSocket::LocalSocketError e)
143{
144 throw ResourceException(method + ": " + msg + " (QLocalSocket::LocalSocketError = " + to_string(e) + ")", e);
145}
146// LCOV_EXCL_STOP
0147
=== added file 'src/local-provider/utils.h'
--- src/local-provider/utils.h 1970-01-01 00:00:00 +0000
+++ src/local-provider/utils.h 2017-03-17 05:04:57 +0000
@@ -0,0 +1,45 @@
1/*
2 * Copyright (C) 2017 Canonical Ltd
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU Lesser General Public License version 3 as
6 * published by the Free Software Foundation.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU Lesser General Public License for more details.
12 *
13 * You should have received a copy of the GNU Lesser General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 *
16 * Authors: Michi Henning <michi.henning@canonical.com>
17 */
18
19#pragma once
20
21#include <boost/filesystem.hpp>
22
23#pragma GCC diagnostic push
24#pragma GCC diagnostic ignored "-Wctor-dtor-privacy"
25#include <QFileDevice>
26#include <QLocalSocket>
27#include <QString>
28#pragma GCC diagnostic pop
29
30#include <string>
31
32constexpr char const* TMPFILE_PREFIX = ".storage-framework";
33
34int64_t get_mtime_nsecs(std::string const& method, std::string const& path);
35bool is_reserved_path(boost::filesystem::path const& path);
36boost::filesystem::path sanitize(std::string const& method, std::string const& name);
37
38[[ noreturn ]]
39void throw_storage_exception(std::string const& method, boost::filesystem::filesystem_error const& e);
40
41[[ noreturn ]]
42void throw_storage_exception(std::string const& method, std::string const& msg, QFileDevice::FileError e);
43
44[[ noreturn ]]
45void throw_storage_exception(std::string const& method, std::string const& msg, QLocalSocket::LocalSocketError e);
046
=== modified file 'src/qt/internal/RuntimeImpl.cpp'
--- src/qt/internal/RuntimeImpl.cpp 2016-11-21 13:29:39 +0000
+++ src/qt/internal/RuntimeImpl.cpp 2017-03-17 05:04:57 +0000
@@ -24,9 +24,11 @@
24#include <unity/storage/qt/internal/AccountImpl.h>24#include <unity/storage/qt/internal/AccountImpl.h>
25#include <unity/storage/qt/internal/AccountsJobImpl.h>25#include <unity/storage/qt/internal/AccountsJobImpl.h>
26#include <unity/storage/qt/internal/StorageErrorImpl.h>26#include <unity/storage/qt/internal/StorageErrorImpl.h>
27#include <unity/storage/qt/Downloader.h>
27#include <unity/storage/qt/ItemJob.h>28#include <unity/storage/qt/ItemJob.h>
28#include <unity/storage/qt/ItemListJob.h>29#include <unity/storage/qt/ItemListJob.h>
29#include <unity/storage/qt/Runtime.h>30#include <unity/storage/qt/Runtime.h>
31#include <unity/storage/qt/Uploader.h>
30#include <unity/storage/qt/VoidJob.h>32#include <unity/storage/qt/VoidJob.h>
31#include <unity/storage/registry/Registry.h>33#include <unity/storage/registry/Registry.h>
3234
@@ -51,10 +53,12 @@
51 qRegisterMetaType<unity::storage::qt::AccountsJob::Status>();53 qRegisterMetaType<unity::storage::qt::AccountsJob::Status>();
52 qRegisterMetaType<unity::storage::qt::Account>();54 qRegisterMetaType<unity::storage::qt::Account>();
53 qRegisterMetaType<QList<unity::storage::qt::Account>>();55 qRegisterMetaType<QList<unity::storage::qt::Account>>();
56 qRegisterMetaType<unity::storage::qt::Downloader::Status>();
54 qRegisterMetaType<unity::storage::qt::Item>();57 qRegisterMetaType<unity::storage::qt::Item>();
55 qRegisterMetaType<QList<unity::storage::qt::Item>>();58 qRegisterMetaType<QList<unity::storage::qt::Item>>();
56 qRegisterMetaType<unity::storage::qt::ItemJob::Status>();59 qRegisterMetaType<unity::storage::qt::ItemJob::Status>();
57 qRegisterMetaType<unity::storage::qt::ItemListJob::Status>();60 qRegisterMetaType<unity::storage::qt::ItemListJob::Status>();
61 qRegisterMetaType<unity::storage::qt::Uploader::Status>();
58 qRegisterMetaType<unity::storage::qt::VoidJob::Status>();62 qRegisterMetaType<unity::storage::qt::VoidJob::Status>();
5963
60 qDBusRegisterMetaType<unity::storage::internal::ItemMetadata>();64 qDBusRegisterMetaType<unity::storage::internal::ItemMetadata>();
6165
=== modified file 'src/qt/internal/StorageErrorImpl.cpp'
--- src/qt/internal/StorageErrorImpl.cpp 2017-01-24 06:12:09 +0000
+++ src/qt/internal/StorageErrorImpl.cpp 2017-03-17 05:04:57 +0000
@@ -78,7 +78,6 @@
78 || type == StorageError::Type::Cancelled78 || type == StorageError::Type::Cancelled
79 || type == StorageError::Type::LogicError79 || type == StorageError::Type::LogicError
80 || type == StorageError::Type::InvalidArgument);80 || type == StorageError::Type::InvalidArgument);
81 assert(!msg.isEmpty());
8281
83 message_ = msg;82 message_ = msg;
84}83}
@@ -87,7 +86,6 @@
87 : StorageErrorImpl(type)86 : StorageErrorImpl(type)
88{87{
89 assert(type == StorageError::Type::NotExists);88 assert(type == StorageError::Type::NotExists);
90 assert(!msg.isEmpty());
9189
92 message_ = msg;90 message_ = msg;
93 item_id_ = key;91 item_id_ = key;
@@ -104,9 +102,6 @@
104 : StorageErrorImpl(type)102 : StorageErrorImpl(type)
105{103{
106 assert(type == StorageError::Type::Exists);104 assert(type == StorageError::Type::Exists);
107 assert(!msg.isEmpty());
108 assert(!item_id.isEmpty());
109 assert(!item_name.isEmpty());
110105
111 message_ = msg;106 message_ = msg;
112 item_id_ = item_id;107 item_id_ = item_id;
@@ -117,7 +112,6 @@
117 : StorageErrorImpl(type)112 : StorageErrorImpl(type)
118{113{
119 assert(type == StorageError::Type::ResourceError);114 assert(type == StorageError::Type::ResourceError);
120 assert(!msg.isEmpty());
121115
122 message_ = msg;116 message_ = msg;
123 error_code_ = error_code;117 error_code_ = error_code;
124118
=== modified file 'tests/CMakeLists.txt'
--- tests/CMakeLists.txt 2017-03-07 10:23:24 +0000
+++ tests/CMakeLists.txt 2017-03-17 05:04:57 +0000
@@ -8,6 +8,7 @@
8set(unit_test_dirs8set(unit_test_dirs
9 registry9 registry
10 local-client10 local-client
11 local-provider
11 remote-client12 remote-client
12 remote-client-v113 remote-client-v1
13 provider-AccountData14 provider-AccountData
1415
=== added directory 'tests/local-provider'
=== added file 'tests/local-provider/CMakeLists.txt'
--- tests/local-provider/CMakeLists.txt 1970-01-01 00:00:00 +0000
+++ tests/local-provider/CMakeLists.txt 2017-03-17 05:04:57 +0000
@@ -0,0 +1,18 @@
1add_executable(local-provider_test local-provider_test.cpp)
2
3add_definitions(-DTEST_DIR="${CMAKE_CURRENT_BINARY_DIR}" -DBOOST_THREAD_VERSION=4)
4
5target_link_libraries(local-provider_test
6 local-provider-lib
7 storage-framework-provider
8 storage-framework-qt-client-v2
9 Qt5::Test
10 ${Boost_LIBRARIES}
11 ${GLIB_DEPS_LIBRARIES}
12 ${GIO_DEPS_LIBRARIES}
13 testutils
14 gtest
15)
16add_test(local-provider local-provider_test)
17
18set(UNIT_TEST_TARGETS ${UNIT_TEST_TARGETS} PARENT_SCOPE)
019
=== added file 'tests/local-provider/local-provider_test.cpp'
--- tests/local-provider/local-provider_test.cpp 1970-01-01 00:00:00 +0000
+++ tests/local-provider/local-provider_test.cpp 2017-03-17 05:04:57 +0000
@@ -0,0 +1,1473 @@
1/*
2 * Copyright (C) 2017 Canonical Ltd
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU Lesser General Public License version 3 as
6 * published by the Free Software Foundation.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU Lesser General Public License for more details.
12 *
13 * You should have received a copy of the GNU Lesser General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 *
16 * Authors: Michi Henning <michi.henning@canonical.com>
17 */
18
19#include "../../src/local-provider/LocalDownloadJob.h"
20#include "../../src/local-provider/LocalProvider.h"
21#include "../../src/local-provider/LocalUploadJob.h"
22
23#include <unity/storage/provider/DownloadJob.h>
24#include <unity/storage/provider/Exceptions.h>
25#include <unity/storage/provider/Server.h>
26#include <unity/storage/qt/client-api.h>
27#include <utils/env_var_guard.h>
28#include <utils/ProviderFixture.h>
29
30#include <boost/algorithm/string.hpp>
31#include <gtest/gtest.h>
32#include <QCoreApplication>
33#include <QSignalSpy>
34
35#include <chrono>
36#include <regex>
37
38#include <fcntl.h>
39
40using namespace unity::storage;
41using namespace std;
42
43namespace
44{
45
46int64_t nanosecs_now()
47{
48 return chrono::system_clock::now().time_since_epoch() / chrono::nanoseconds(1);
49}
50
51string const ROOT_DIR = TEST_DIR "/data";
52
53class LocalProviderTest : public ProviderFixture
54{
55protected:
56 void SetUp() override
57 {
58 boost::filesystem::remove_all(ROOT_DIR);
59 boost::filesystem::create_directory(ROOT_DIR);
60
61 ProviderFixture::SetUp();
62 runtime_.reset(new qt::Runtime(connection()));
63 acc_ = runtime_->make_test_account(service_connection_->baseService(), object_path());
64 }
65
66 void TearDown() override
67 {
68 runtime_.reset();
69 ProviderFixture::TearDown();
70 }
71
72 unique_ptr<qt::Runtime> runtime_;
73 qt::Account acc_;
74};
75
76constexpr int SIGNAL_WAIT_TIME = 30000;
77
78template <typename Job>
79void wait(Job* job)
80{
81 QSignalSpy spy(job, &Job::statusChanged);
82 while (job->status() == Job::Loading)
83 {
84 if (!spy.wait(SIGNAL_WAIT_TIME))
85 {
86 throw runtime_error("Wait for statusChanged signal timed out");
87 }
88 }
89}
90
91qt::Item get_root(qt::Account const& account)
92{
93 unique_ptr<qt::ItemListJob> j(account.roots());
94 assert(j->isValid());
95 QSignalSpy ready_spy(j.get(), &qt::ItemListJob::itemsReady);
96 assert(ready_spy.wait(SIGNAL_WAIT_TIME));
97 auto arg = ready_spy.takeFirst();
98 auto items = qvariant_cast<QList<qt::Item>>(arg.at(0));
99 assert(items.size() == 1);
100 return items[0];
101}
102
103QList<qt::Item> get_items(qt::ItemListJob *job)
104{
105 QList<qt::Item> items;
106 auto connection = QObject::connect(
107 job, &qt::ItemListJob::itemsReady,
108 [&](QList<qt::Item> const& new_items)
109 {
110 items.append(new_items);
111 });
112 try
113 {
114 wait(job);
115 }
116 catch (...)
117 {
118 QObject::disconnect(connection);
119 throw;
120 }
121 QObject::disconnect(connection);
122 return items;
123}
124
125const string file_contents =
126 "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do "
127 "eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut "
128 "enim ad minim veniam, quis nostrud exercitation ullamco laboris "
129 "nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor "
130 "in reprehenderit in voluptate velit esse cillum dolore eu fugiat "
131 "nulla pariatur. Excepteur sint occaecat cupidatat non proident, "
132 "sunt in culpa qui officia deserunt mollit anim id est laborum.\n";
133
134} // namespace
135
136TEST(Directories, env_vars)
137{
138 // These tests cause the constructor to throw, so we instantiate the provider directly.
139
140 {
141 EnvVarGuard env("SF_LOCAL_PROVIDER_ROOT", "/no_such_dir");
142
143 try
144 {
145 LocalProvider();
146 FAIL();
147 }
148 catch (provider::InvalidArgumentException const& e)
149 {
150 EXPECT_STREQ("InvalidArgumentException: LocalProvider(): Environment variable "
151 "SF_LOCAL_PROVIDER_ROOT must denote an existing directory",
152 e.what());
153 }
154 }
155
156 {
157 EnvVarGuard env("SF_LOCAL_PROVIDER_ROOT", TEST_DIR "/Makefile");
158
159 try
160 {
161 LocalProvider();
162 FAIL();
163 }
164 catch (provider::InvalidArgumentException const& e)
165 {
166 EXPECT_STREQ("InvalidArgumentException: LocalProvider(): Environment variable "
167 "SF_LOCAL_PROVIDER_ROOT must denote an existing directory",
168 e.what());
169 }
170 }
171
172 {
173 string const dir = TEST_DIR "/noperm";
174
175 mkdir(dir.c_str(), 0555);
176 ASSERT_EQ(0, chmod(dir.c_str(), 0555)); // In case dir was there already.
177
178 EnvVarGuard env1("SF_LOCAL_PROVIDER_ROOT", nullptr);
179 EnvVarGuard env2("XDG_DATA_HOME", dir.c_str());
180
181 using namespace boost::filesystem;
182
183 try
184 {
185 LocalProvider();
186 ASSERT_EQ(0, chmod(dir.c_str(), 0775));
187 remove_all(dir);
188 FAIL();
189 }
190 catch (provider::PermissionException const& e)
191 {
192 EXPECT_EQ(string("PermissionException: LocalProvider(): \"") + dir + "/storage-framework\": "
193 "boost::filesystem::create_directories: Permission denied: \"" + dir + "/storage-framework\"",
194 e.what());
195 }
196
197 ASSERT_EQ(0, chmod(dir.c_str(), 0775));
198 ASSERT_TRUE(remove_all(dir));
199
200 // Try again, which must succeed now (for coverage).
201 LocalProvider();
202 ASSERT_TRUE(is_directory(dir + "/storage-framework/local"));
203 ASSERT_TRUE(remove_all(dir));
204 }
205
206 {
207 string const dir = TEST_DIR "/snap_user_common";
208 mkdir(dir.c_str(), 0775);
209
210 EnvVarGuard env1("SF_LOCAL_PROVIDER_ROOT", nullptr);
211 EnvVarGuard env2("SNAP_USER_COMMON", dir.c_str());
212
213 using namespace boost::filesystem;
214
215 LocalProvider();
216 ASSERT_TRUE(exists(dir + "/storage-framework/local"));
217 ASSERT_TRUE(remove_all(dir));
218 }
219}
220
221TEST_F(LocalProviderTest, basic)
222{
223 using namespace unity::storage::qt;
224
225 set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
226
227 // Basic sanity check, get the root.
228 unique_ptr<ItemListJob> j(acc_.roots());
229 EXPECT_TRUE(j->isValid());
230 QSignalSpy ready_spy(j.get(), &ItemListJob::itemsReady);
231 ASSERT_TRUE(ready_spy.wait(SIGNAL_WAIT_TIME));
232 ASSERT_EQ(1, ready_spy.count());
233 auto arg = ready_spy.takeFirst();
234 auto items = qvariant_cast<QList<Item>>(arg.at(0));
235 ASSERT_EQ(1, items.size());
236
237 // Check contents of returned item.
238 auto root = items[0];
239 EXPECT_TRUE(root.isValid());
240 EXPECT_EQ(Item::Type::Root, root.type());
241 EXPECT_EQ(ROOT_DIR, root.itemId().toStdString());
242 EXPECT_EQ("/", root.name());
243 EXPECT_EQ("", root.etag());
244 EXPECT_EQ(QList<QString>(), root.parentIds());
245 qDebug() << root.lastModifiedTime();
246 EXPECT_TRUE(root.lastModifiedTime().isValid());
247 EXPECT_EQ(acc_, root.account());
248
249 ASSERT_EQ(5, root.metadata().size());
250 auto free_space_bytes = root.metadata().value("free_space_bytes").toULongLong();
251 cout << "free_space_bytes: " << free_space_bytes << endl;
252 EXPECT_GT(free_space_bytes, 0);
253 auto used_space_bytes = root.metadata().value("used_space_bytes").toULongLong();
254 cout << "used_space_bytes: " << used_space_bytes << endl;
255 EXPECT_GT(used_space_bytes, 0);
256 auto content_type = root.metadata().value("content_type").toString();
257 EXPECT_EQ("inode/directory", content_type);
258 auto writable = root.metadata().value("writable").toBool();
259 EXPECT_TRUE(writable);
260
261 // yyyy-mm-ddThh:mm:ssZ
262 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$";
263 string mtime = root.metadata().value("last_modified_time").toString().toStdString();
264 cout << "last_modified_time: " << mtime << endl;
265 regex re(date_time_fmt);
266 EXPECT_TRUE(regex_match(mtime, re));
267}
268
269TEST_F(LocalProviderTest, create_folder)
270{
271 {
272 using namespace unity::storage::qt;
273
274 set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
275
276 auto root = get_root(acc_);
277 unique_ptr<ItemJob> job(root.createFolder("child"));
278 wait(job.get());
279 ASSERT_EQ(ItemJob::Finished, job->status()) << job->error().errorString().toStdString();
280
281 Item child = job->item();
282 EXPECT_EQ(ROOT_DIR + "/child", child.itemId().toStdString());
283 EXPECT_EQ("child", child.name().toStdString());
284 ASSERT_EQ(1, child.parentIds().size());
285 EXPECT_EQ(ROOT_DIR, child.parentIds().at(0).toStdString());
286 EXPECT_EQ("", child.etag());
287 EXPECT_EQ(Item::Type::Folder, child.type());
288 EXPECT_EQ(5, child.metadata().size());
289
290 struct stat st;
291 ASSERT_EQ(0, stat(child.itemId().toStdString().c_str(), &st));
292 EXPECT_TRUE(S_ISDIR(st.st_mode));
293
294 // Again, to get coverage for a StorageException caught in invoke_async().
295 job.reset(root.createFolder("child"));
296 wait(job.get());
297 ASSERT_EQ(ItemJob::Error, job->status()) << job->error().errorString().toStdString();
298 EXPECT_EQ(string("Exists: create_folder(): \"") + ROOT_DIR + "/child\" exists already",
299 job->error().errorString().toStdString());
300
301 // Again, without write permission on the root dir, to get coverage for a filesystem_error in invoke_async().
302 ASSERT_EQ(0, ::rmdir((ROOT_DIR + "/child").c_str()));
303 ASSERT_EQ(0, ::chmod(ROOT_DIR.c_str(), 0555));
304 job.reset(root.createFolder("child"));
305 wait(job.get());
306 ::chmod(ROOT_DIR.c_str(), 0755);
307 ASSERT_EQ(ItemJob::Error, job->status()) << job->error().errorString().toStdString();
308 EXPECT_EQ(string("PermissionDenied: create_folder(): \"") + ROOT_DIR
309 + "/child\": boost::filesystem::create_directory: Permission denied: \"" + ROOT_DIR + "/child\"",
310 job->error().errorString().toStdString());
311 }
312}
313
314TEST_F(LocalProviderTest, delete_item)
315{
316 {
317 using namespace unity::storage::qt;
318
319 set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
320
321 auto root = get_root(acc_);
322 unique_ptr<ItemJob> job(root.createFolder("child"));
323 wait(job.get());
324 ASSERT_EQ(ItemJob::Finished, job->status()) << job->error().errorString().toStdString();
325
326 Item child = job->item();
327 unique_ptr<VoidJob> delete_job(child.deleteItem());
328 wait(delete_job.get());
329 ASSERT_EQ(ItemJob::Finished, delete_job->status()) << delete_job->error().errorString().toStdString();
330
331 struct stat st;
332 ASSERT_EQ(-1, stat(child.itemId().toStdString().c_str(), &st));
333 EXPECT_EQ(ENOENT, errno);
334 }
335}
336
337TEST_F(LocalProviderTest, delete_item_noperm)
338{
339 {
340 using namespace unity::storage::qt;
341
342 set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
343
344 auto root = get_root(acc_);
345 unique_ptr<ItemJob> job(root.createFolder("child"));
346 wait(job.get());
347 ASSERT_EQ(ItemJob::Finished, job->status()) << job->error().errorString().toStdString();
348 }
349}
350
351TEST_F(LocalProviderTest, delete_root)
352{
353 // Client-side API does not allow us to try to delete the root, so we talk to the provider directly.
354 auto p = make_shared<LocalProvider>();
355
356 auto fut = p->delete_item(ROOT_DIR, provider::Context());
357 try
358 {
359 fut.get();
360 FAIL();
361 }
362 catch (provider::LogicException const& e)
363 {
364 EXPECT_STREQ("LogicException: delete_item(): cannot delete root", e.what());
365 }
366}
367
368TEST_F(LocalProviderTest, metadata)
369{
370 // Client-side API does not call the Metadata DBus method (except as part of parents()),
371 // so we talk to the provider directly.
372 auto p = make_shared<LocalProvider>();
373
374 auto fut = p->metadata(ROOT_DIR, {}, provider::Context());
375 auto item = fut.get();
376 EXPECT_EQ(5, item.metadata.size());
377
378 // Again, to get coverage for the "not file or folder" case in make_item().
379 ASSERT_EQ(0, mknod((ROOT_DIR + "/pipe").c_str(), S_IFIFO | 06666, 0));
380 try
381 {
382 auto fut = p->metadata(ROOT_DIR + "/pipe", {}, provider::Context());
383 fut.get();
384 FAIL();
385 }
386 catch (provider::NotExistsException const& e)
387 {
388 EXPECT_EQ(string("NotExistsException: metadata(): \"") + ROOT_DIR + "/pipe\" is neither a file nor a folder",
389 e.what());
390 }
391}
392
393TEST_F(LocalProviderTest, lookup)
394{
395 using namespace unity::storage::qt;
396
397 set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
398
399 auto root = get_root(acc_);
400 {
401 unique_ptr<ItemJob> job(root.createFolder("child"));
402 wait(job.get());
403 ASSERT_EQ(ItemJob::Finished, job->status()) << job->error().errorString().toStdString();
404 }
405
406 unique_ptr<ItemListJob> job(root.lookup("child"));
407 auto items = get_items(job.get());
408 ASSERT_EQ(1, items.size());
409 auto child = items.at(0);
410
411 EXPECT_EQ(ROOT_DIR + "/child", child.itemId().toStdString());
412 EXPECT_EQ("child", child.name().toStdString());
413 ASSERT_EQ(1, child.parentIds().size());
414 EXPECT_EQ(ROOT_DIR, child.parentIds().at(0).toStdString());
415 EXPECT_EQ("", child.etag());
416 EXPECT_EQ(Item::Type::Folder, child.type());
417 EXPECT_EQ(5, child.metadata().size());
418
419 // Remove the child again and try the lookup once more.
420 ASSERT_EQ(0, rmdir((ROOT_DIR + "/child").c_str()));
421
422 job.reset(root.lookup("child"));
423 wait(job.get());
424 EXPECT_EQ(ItemJob::Error, job->status());
425 EXPECT_EQ(string("NotExists: lookup(): \"") + ROOT_DIR + "/child\": boost::filesystem::canonical: "
426 + "No such file or directory: \"" + ROOT_DIR + "/child\"",
427 job->error().errorString().toStdString());
428}
429
430TEST_F(LocalProviderTest, list)
431{
432 using namespace unity::storage::qt;
433
434 set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
435
436 auto root = get_root(acc_);
437 {
438 unique_ptr<ItemJob> job(root.createFolder("child"));
439 wait(job.get());
440 ASSERT_EQ(ItemJob::Finished, job->status()) << job->error().errorString().toStdString();
441 }
442
443 // Make a weird item that will be ignored (for coverage).
444 ASSERT_EQ(0, mknod((ROOT_DIR + "/pipe").c_str(), S_IFIFO | 06666, 0));
445
446 // Make a file that starts with the temp file prefix (for coverage).
447 int fd = creat((ROOT_DIR + "/.storage-framework").c_str(), 0755);
448 ASSERT_GT(fd, 0);
449 close(fd);
450
451 unique_ptr<ItemListJob> job(root.list());
452 auto items = get_items(job.get());
453 ASSERT_EQ(1, items.size());
454 auto child = items.at(0);
455
456 EXPECT_EQ(ROOT_DIR + "/child", child.itemId().toStdString());
457 EXPECT_EQ("child", child.name().toStdString());
458 ASSERT_EQ(1, child.parentIds().size());
459 EXPECT_EQ(ROOT_DIR, child.parentIds().at(0).toStdString());
460 EXPECT_EQ("", child.etag());
461 EXPECT_EQ(Item::Type::Folder, child.type());
462 EXPECT_EQ(5, child.metadata().size());
463}
464
465void make_hierarchy()
466{
467 // Make a small tree so we have something to test with for move() and copy().
468 ASSERT_EQ(0, mkdir((ROOT_DIR + "/a").c_str(), 0755));
469 ASSERT_EQ(0, mkdir((ROOT_DIR + "/a/b").c_str(), 0755));
470 string cmd = string("echo hello >") + ROOT_DIR + "/hello";
471 ASSERT_EQ(0, system(cmd.c_str()));
472 cmd = string("echo text >") + ROOT_DIR + "/a/foo.txt";
473 ASSERT_EQ(0, system(cmd.c_str()));
474 ASSERT_EQ(0, mknod((ROOT_DIR + "/a/pipe").c_str(), S_IFIFO | 06666, 0));
475 ASSERT_EQ(0, mkdir((ROOT_DIR + "/a/.storage-framework-").c_str(), 0755));
476 ASSERT_EQ(0, mkdir((ROOT_DIR + "/a/b/.storage-framework-").c_str(), 0755));
477 ASSERT_EQ(0, mknod((ROOT_DIR + "/a/b/pipe").c_str(), S_IFIFO | 06666, 0));
478}
479
480TEST_F(LocalProviderTest, move)
481{
482 using namespace unity::storage::qt;
483
484 set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
485
486 auto start_time = nanosecs_now();
487
488 make_hierarchy();
489
490 auto root = get_root(acc_);
491
492 qt::Item hello;
493 {
494 unique_ptr<ItemListJob> job(root.lookup("hello"));
495 auto items = get_items(job.get());
496 ASSERT_EQ(ItemListJob::Finished, job->status()) << job->error().errorString().toStdString();
497 ASSERT_EQ(1, items.size());
498 hello = items.at(0);
499 }
500
501 struct stat st;
502 ASSERT_EQ(0, stat(hello.itemId().toStdString().c_str(), &st));
503 auto old_ino = st.st_ino;
504
505 // Check metadata.
506 EXPECT_EQ("hello", hello.name());
507 ASSERT_EQ(1, hello.parentIds().size());
508 EXPECT_EQ(ROOT_DIR, hello.parentIds().at(0).toStdString());
509 EXPECT_EQ(Item::Type::File, hello.type());
510
511 ASSERT_EQ(6, hello.metadata().size());
512 auto free_space_bytes = hello.metadata().value("free_space_bytes").toLongLong();
513 cout << "free_space_bytes: " << free_space_bytes << endl;
514 EXPECT_GT(free_space_bytes, 0);
515 auto used_space_bytes = hello.metadata().value("used_space_bytes").toLongLong();
516 cout << "used_space_bytes: " << used_space_bytes << endl;
517 EXPECT_GT(used_space_bytes, 0);
518 auto content_type = hello.metadata().value("content_type").toString();
519 EXPECT_EQ("application/octet-stream", content_type);
520 auto writable = hello.metadata().value("writable").toBool();
521 EXPECT_TRUE(writable);
522 auto size = hello.metadata().value("size_in_bytes").toLongLong();
523 EXPECT_EQ(6, size);
524
525 // yyyy-mm-ddThh:mm:ssZ
526 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$";
527 string date_time = hello.metadata().value("last_modified_time").toString().toStdString();
528 cout << "last_modified_time: " << date_time << endl;
529 regex re(date_time_fmt);
530 EXPECT_TRUE(regex_match(date_time, re));
531
532 // Check that the file was modified in the last two seconds.
533 // Because the system clock can tick a lot more frequently than the file system time stamp,
534 // we allow the mtime to be up to one second *earlier* than the time we started the operation.
535 string mtime_str = hello.etag().toStdString();
536 char* end;
537 int64_t mtime = strtoll(mtime_str.c_str(), &end, 10);
538 EXPECT_LE(start_time - 1000000000, mtime);
539 EXPECT_LT(mtime, start_time + 2000000000);
540
541 // Move hello -> world
542 qt::Item world;
543 {
544 unique_ptr<ItemJob> job(hello.move(root, "world"));
545 wait(job.get());
546 ASSERT_EQ(ItemJob::Finished, job->status()) << job->error().errorString().toStdString();
547 world = job->item();
548 }
549 EXPECT_FALSE(boost::filesystem::exists(hello.itemId().toStdString()));
550 EXPECT_EQ(ROOT_DIR + "/world", world.itemId().toStdString());
551
552 ASSERT_EQ(0, stat(world.itemId().toStdString().c_str(), &st));
553 auto new_ino = st.st_ino;
554 EXPECT_EQ(old_ino, new_ino);
555
556 // For coverage: try moving world -> a (which must fail)
557 unique_ptr<ItemJob> job(world.move(root, "a"));
558 wait(job.get());
559 ASSERT_EQ(ItemJob::Error, job->status()) << job->error().errorString().toStdString();
560 EXPECT_EQ(string("Exists: move(): \"") + ROOT_DIR + "/a\" exists already",
561 job->error().errorString().toStdString());
562}
563
564TEST_F(LocalProviderTest, copy_file)
565{
566 using namespace unity::storage::qt;
567
568 set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
569
570 make_hierarchy();
571
572 auto root = get_root(acc_);
573
574 // Copy hello -> world
575 qt::Item hello;
576 {
577 unique_ptr<ItemListJob> job(root.lookup("hello"));
578 auto items = get_items(job.get());
579 ASSERT_EQ(ItemListJob::Finished, job->status()) << job->error().errorString().toStdString();
580 ASSERT_EQ(1, items.size());
581 hello = items.at(0);
582 }
583
584 struct stat st;
585 ASSERT_EQ(0, stat(hello.itemId().toStdString().c_str(), &st));
586 auto old_ino = st.st_ino;
587
588 qt::Item world;
589 {
590 unique_ptr<ItemJob> job(hello.copy(root, "world"));
591 wait(job.get());
592 ASSERT_EQ(ItemJob::Finished, job->status()) << job->error().errorString().toStdString();
593 world = job->item();
594 }
595 EXPECT_TRUE(boost::filesystem::exists(hello.itemId().toStdString()));
596 EXPECT_EQ(ROOT_DIR + "/world", world.itemId().toStdString());
597
598 ASSERT_EQ(0, stat(world.itemId().toStdString().c_str(), &st));
599 auto new_ino = st.st_ino;
600 EXPECT_NE(old_ino, new_ino);
601}
602
603TEST_F(LocalProviderTest, copy_tree)
604{
605 using namespace unity::storage::qt;
606 using namespace boost::filesystem;
607
608 set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
609
610 make_hierarchy();
611
612 auto root = get_root(acc_);
613
614 // Copy a -> c
615 qt::Item a;
616 {
617 unique_ptr<ItemListJob> job(root.lookup("a"));
618 auto items = get_items(job.get());
619 ASSERT_EQ(ItemListJob::Finished, job->status()) << job->error().errorString().toStdString();
620 ASSERT_EQ(1, items.size());
621 a = items.at(0);
622 }
623
624 qt::Item c;
625 {
626 unique_ptr<ItemJob> job(a.copy(root, "c"));
627 wait(job.get());
628 ASSERT_EQ(ItemJob::Finished, job->status()) << job->error().errorString().toStdString();
629 c = job->item();
630 }
631 EXPECT_TRUE(exists(c.itemId().toStdString()));
632
633 // Check that we only copied regular files and directories, but not a pipe or anything starting with
634 // the temp file prefix.
635 EXPECT_TRUE(exists(ROOT_DIR + "/c/b"));
636 EXPECT_TRUE(exists(ROOT_DIR + "/c/foo.txt"));
637 EXPECT_FALSE(exists(ROOT_DIR + "/c/pipe"));
638 EXPECT_FALSE(exists(ROOT_DIR + "/c/storage-framework-"));
639 EXPECT_FALSE(exists(ROOT_DIR + "/c/b/pipe"));
640 EXPECT_FALSE(exists(ROOT_DIR + "/c/b/storage-framework-"));
641
642 // Copy c -> a. This must fail because a exists.
643 {
644 unique_ptr<ItemJob> job(c.copy(root, "a"));
645 wait(job.get());
646 ASSERT_EQ(ItemJob::Error, job->status()) << job->error().errorString().toStdString();
647 EXPECT_EQ(string("Exists: copy(): \"") + ROOT_DIR + "/a\" exists already",
648 job->error().errorString().toStdString());
649 }
650}
651
652TEST_F(LocalProviderTest, download)
653{
654 using namespace unity::storage::qt;
655
656 set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
657
658 int const segments = 10000;
659 string large_contents;
660 large_contents.reserve(file_contents.size() * segments);
661 for (int i = 0; i < segments; i++)
662 {
663 large_contents += file_contents;
664 }
665 string const full_path = ROOT_DIR + "/foo.txt";
666 {
667 int fd = open(full_path.c_str(), O_WRONLY | O_CREAT | O_EXCL, 0644);
668 ASSERT_GT(fd, 0);
669 ASSERT_EQ(ssize_t(large_contents.size()), write(fd, &large_contents[0], large_contents.size())) << strerror(errno);
670 ASSERT_EQ(0, close(fd));
671 }
672
673 unique_ptr<ItemJob> job(acc_.get(QString::fromStdString(full_path)));
674 wait(job.get());
675 EXPECT_TRUE(job->isValid());
676
677 auto file = job->item();
678 unique_ptr<Downloader> downloader(file.createDownloader(Item::ErrorIfConflict));
679
680 int64_t n_read = 0;
681 QObject::connect(downloader.get(), &QIODevice::readyRead,
682 [&]() {
683 auto bytes = downloader->readAll();
684 string const expected = large_contents.substr(n_read, bytes.size());
685 EXPECT_EQ(expected, bytes.toStdString());
686 n_read += bytes.size();
687 });
688 QSignalSpy read_finished_spy(downloader.get(), &QIODevice::readChannelFinished);
689 ASSERT_TRUE(read_finished_spy.wait(SIGNAL_WAIT_TIME));
690
691 QSignalSpy status_spy(downloader.get(), &Downloader::statusChanged);
692 downloader->close();
693 while (downloader->status() == Downloader::Ready)
694 {
695 ASSERT_TRUE(status_spy.wait(SIGNAL_WAIT_TIME));
696 }
697 ASSERT_EQ(Downloader::Finished, downloader->status()) << downloader->error().errorString().toStdString();
698
699 EXPECT_EQ(int64_t(large_contents.size()), n_read);
700}
701
702TEST_F(LocalProviderTest, download_short_read)
703{
704 using namespace unity::storage::qt;
705
706 set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
707
708 int const segments = 10000;
709 string const full_path = ROOT_DIR + "/foo.txt";
710 {
711 int fd = open(full_path.c_str(), O_WRONLY | O_CREAT | O_EXCL, 0644);
712 ASSERT_GT(fd, 0);
713 for (int i = 0; i < segments; i++)
714 {
715 ASSERT_EQ(ssize_t(file_contents.size()), write(fd, &file_contents[0], file_contents.size())) << strerror(errno);
716 }
717 ASSERT_EQ(0, close(fd));
718 }
719
720 unique_ptr<ItemJob> job(acc_.get(QString::fromStdString(full_path)));
721 wait(job.get());
722 ASSERT_EQ(ItemJob::Finished, job->status()) << job->error().errorString().toStdString();
723
724 auto file = job->item();
725 unique_ptr<Downloader> downloader(file.createDownloader(Item::ErrorIfConflict));
726
727 QSignalSpy spy(downloader.get(), &Downloader::statusChanged);
728 while (downloader->status() == Downloader::Loading)
729 {
730 ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME));
731 }
732
733 downloader->close();
734 while (downloader->status() == Downloader::Ready)
735 {
736 ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME));
737 }
738 ASSERT_EQ(Downloader::Error, downloader->status()) << downloader->error().errorString().toStdString();
739
740 auto error = downloader->error();
741 EXPECT_EQ(qt::StorageError::LogicError, error.type());
742 cout << error.message().toStdString() << endl;
743 EXPECT_TRUE(boost::starts_with(error.message().toStdString(),
744 "finish() method called too early, file \""
745 + full_path + "\" has size 4460000 but only"));
746}
747
748TEST_F(LocalProviderTest, download_etag_mismatch)
749{
750 using namespace unity::storage::qt;
751
752 set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
753
754 string const full_path = ROOT_DIR + "/foo.txt";
755 string cmd = string("echo hello >") + full_path;
756 ASSERT_EQ(0, system(cmd.c_str()));
757
758 unique_ptr<ItemJob> job(acc_.get(QString::fromStdString(full_path)));
759 wait(job.get());
760 ASSERT_EQ(ItemJob::Finished, job->status()) << job->error().errorString().toStdString();
761
762 auto file = job->item();
763
764 sleep(1);
765 cmd = string("touch ") + full_path;
766 ASSERT_EQ(0, system(cmd.c_str()));
767
768 unique_ptr<Downloader> downloader(file.createDownloader(Item::ErrorIfConflict));
769
770 QSignalSpy spy(downloader.get(), &Downloader::statusChanged);
771 while (downloader->status() != Downloader::Error)
772 {
773 ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME));
774 }
775
776 auto error = downloader->error();
777 EXPECT_EQ(qt::StorageError::Conflict, error.type());
778 EXPECT_EQ("download(): etag mismatch", error.message().toStdString());
779}
780
781TEST_F(LocalProviderTest, download_wrong_file_type)
782{
783 // We can't try a download for a directory via the client API, so we use the LocalDownloadJob directly.
784
785 auto p = make_shared<LocalProvider>();
786
787 string const dir = ROOT_DIR + "/dir";
788 ASSERT_EQ(0, mkdir(dir.c_str(), 0755));
789
790 try
791 {
792 LocalDownloadJob(p, dir, "some_etag");
793 FAIL();
794 }
795 catch (provider::InvalidArgumentException const& e)
796 {
797 EXPECT_EQ(string("InvalidArgumentException: download(): \"" + dir + "\" is not a file"), e.what());
798 }
799}
800
801TEST_F(LocalProviderTest, download_no_permission)
802{
803 using namespace unity::storage::qt;
804
805 set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
806
807 string const full_path = ROOT_DIR + "/foo.txt";
808 string cmd = string("echo hello >") + full_path;
809 ASSERT_EQ(0, system(cmd.c_str()));
810
811 unique_ptr<ItemJob> job(acc_.get(QString::fromStdString(full_path)));
812 wait(job.get());
813 ASSERT_EQ(ItemJob::Finished, job->status()) << job->error().errorString().toStdString();
814
815 auto file = job->item();
816
817 ASSERT_EQ(0, chmod(full_path.c_str(), 0244));
818
819 unique_ptr<Downloader> downloader(file.createDownloader(Item::ErrorIfConflict));
820
821 QSignalSpy spy(downloader.get(), &Downloader::statusChanged);
822 while (downloader->status() != Downloader::Error)
823 {
824 ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME));
825 }
826
827 auto error = downloader->error();
828 EXPECT_EQ(qt::StorageError::ResourceError, error.type());
829 EXPECT_EQ(string("download(): : cannot open \"") + full_path + "\": Permission denied (QFileDevice::FileError = 5)",
830 error.message().toStdString());
831}
832
833TEST_F(LocalProviderTest, download_no_such_file)
834{
835 // We can't try a download for a non-existent file via the client API, so we use the LocalDownloadJob directly.
836
837 auto p = make_shared<LocalProvider>();
838
839 try
840 {
841 LocalDownloadJob(p, ROOT_DIR + "/no_such_file", "some_etag");
842 FAIL();
843 }
844 catch (provider::NotExistsException const&)
845 {
846 }
847}
848
849TEST_F(LocalProviderTest, update)
850{
851 using namespace unity::storage::qt;
852
853 set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
854
855 auto full_path = ROOT_DIR + "/foo.txt";
856 auto cmd = string("echo hello >") + full_path;
857 ASSERT_EQ(0, system(cmd.c_str()));
858
859 unique_ptr<ItemJob> job(acc_.get(QString::fromStdString(full_path)));
860 wait(job.get());
861 EXPECT_TRUE(job->isValid());
862
863 auto file = job->item();
864 auto old_etag = file.etag();
865
866 int const segments = 50;
867 unique_ptr<Uploader> uploader(file.createUploader(Item::ErrorIfConflict, file_contents.size() * segments));
868
869 int count = 0;
870 QTimer timer;
871 timer.setSingleShot(false);
872 timer.setInterval(10);
873 QObject::connect(&timer, &QTimer::timeout, [&] {
874 uploader->write(&file_contents[0], file_contents.size());
875 count++;
876 if (count == segments)
877 {
878 uploader->close();
879 }
880 });
881
882 QSignalSpy spy(uploader.get(), &Uploader::statusChanged);
883 timer.start();
884 while (uploader->status() == Uploader::Loading ||
885 uploader->status() == Uploader::Ready)
886 {
887 ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME));
888 }
889 ASSERT_EQ(Uploader::Finished, uploader->status()) << uploader->error().errorString().toStdString();
890
891 file = uploader->item();
892 EXPECT_NE(old_etag, file.etag());
893 EXPECT_EQ(int64_t(file_contents.size() * segments), file.sizeInBytes());
894}
895
896TEST_F(LocalProviderTest, update_empty)
897{
898 using namespace unity::storage::qt;
899
900 set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
901
902 auto full_path = ROOT_DIR + "/foo.txt";
903 auto cmd = string("echo hello >") + full_path;
904 ASSERT_EQ(0, system(cmd.c_str()));
905
906 unique_ptr<ItemJob> job(acc_.get(QString::fromStdString(full_path)));
907 wait(job.get());
908 EXPECT_TRUE(job->isValid());
909
910 auto file = job->item();
911 auto old_etag = file.etag();
912
913 sleep(1); // Make sure mtime changes.
914 unique_ptr<Uploader> uploader(file.createUploader(Item::ErrorIfConflict, 0));
915 {
916 QSignalSpy spy(uploader.get(), &Uploader::statusChanged);
917 while (uploader->status() != Uploader::Ready)
918 {
919 ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME));
920 }
921 }
922
923 QSignalSpy spy(uploader.get(), &Uploader::statusChanged);
924 uploader->close();
925 while (uploader->status() != Uploader::Finished)
926 {
927 ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME));
928 }
929
930 file = uploader->item();
931 EXPECT_NE(old_etag, file.etag());
932 EXPECT_EQ(int64_t(0), file.sizeInBytes());
933}
934
935TEST_F(LocalProviderTest, update_cancel)
936{
937 using namespace unity::storage::qt;
938
939 set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
940
941 auto full_path = ROOT_DIR + "/foo.txt";
942 auto cmd = string("echo hello >") + full_path;
943 ASSERT_EQ(0, system(cmd.c_str()));
944
945 unique_ptr<ItemJob> job(acc_.get(QString::fromStdString(full_path)));
946 wait(job.get());
947 EXPECT_TRUE(job->isValid());
948
949 auto file = job->item();
950 auto old_etag = file.etag();
951
952 int const segments = 50;
953 unique_ptr<Uploader> uploader(file.createUploader(Item::ErrorIfConflict, file_contents.size() * segments));
954
955 int count = 0;
956 QTimer timer;
957 timer.setSingleShot(false);
958 timer.setInterval(10);
959 QObject::connect(&timer, &QTimer::timeout, [&] {
960 uploader->write(&file_contents[0], file_contents.size());
961 count++;
962 if (count == segments / 2)
963 {
964 uploader->cancel();
965 }
966 else if (count == segments)
967 {
968 uploader->close();
969 }
970 });
971
972 QSignalSpy spy(uploader.get(), &Uploader::statusChanged);
973 timer.start();
974 while (uploader->status() != Uploader::Cancelled)
975 {
976 ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME));
977 }
978}
979
980TEST_F(LocalProviderTest, update_file_touched_before_uploading)
981{
982 using namespace unity::storage::qt;
983
984 set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
985
986 auto full_path = ROOT_DIR + "/foo.txt";
987 auto cmd = string("echo hello >") + full_path;
988 ASSERT_EQ(0, system(cmd.c_str()));
989
990 unique_ptr<ItemJob> job(acc_.get(QString::fromStdString(full_path)));
991 wait(job.get());
992 EXPECT_TRUE(job->isValid());
993
994 auto file = job->item();
995 auto old_etag = file.etag();
996
997 sleep(1); // Make sure mtime changes.
998 cmd = string("touch ") + full_path;
999 ASSERT_EQ(0, system(cmd.c_str()));
1000 unique_ptr<Uploader> uploader(file.createUploader(Item::ErrorIfConflict, 0));
1001 QSignalSpy spy(uploader.get(), &Uploader::statusChanged);
1002 while (uploader->status() != Uploader::Error)
1003 {
1004 ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME));
1005 }
1006 EXPECT_EQ("update(): etag mismatch", uploader->error().message().toStdString());
1007}
1008
1009TEST_F(LocalProviderTest, update_file_touched_while_uploading)
1010{
1011 using namespace unity::storage::qt;
1012
1013 set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
1014
1015 auto full_path = ROOT_DIR + "/foo.txt";
1016 auto cmd = string("echo hello >") + full_path;
1017 ASSERT_EQ(0, system(cmd.c_str()));
1018
1019 unique_ptr<ItemJob> job(acc_.get(QString::fromStdString(full_path)));
1020 wait(job.get());
1021 EXPECT_TRUE(job->isValid());
1022
1023 auto file = job->item();
1024 auto old_etag = file.etag();
1025
1026 int const segments = 50;
1027 unique_ptr<Uploader> uploader(file.createUploader(Item::ErrorIfConflict, file_contents.size() * segments));
1028
1029 int count = 0;
1030 QTimer timer;
1031 timer.setSingleShot(false);
1032 timer.setInterval(10);
1033 QObject::connect(&timer, &QTimer::timeout, [&] {
1034 uploader->write(&file_contents[0], file_contents.size());
1035 count++;
1036 if (count == segments / 2)
1037 {
1038 sleep(1);
1039 cmd = string("touch ") + full_path;
1040 ASSERT_EQ(0, system(cmd.c_str()));
1041 }
1042 else if (count == segments)
1043 {
1044 uploader->close();
1045 }
1046 });
1047
1048 QSignalSpy spy(uploader.get(), &Uploader::statusChanged);
1049 timer.start();
1050 while (uploader->status() != Uploader::Error)
1051 {
1052 ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME));
1053 }
1054 ASSERT_EQ(Uploader::Error, uploader->status());
1055 EXPECT_EQ("update(): etag mismatch", uploader->error().message().toStdString());
1056}
1057
1058TEST_F(LocalProviderTest, update_ignore_etag_mismatch)
1059{
1060 using namespace unity::storage::qt;
1061
1062 set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
1063
1064 auto full_path = ROOT_DIR + "/foo.txt";
1065 auto cmd = string("echo hello >") + full_path;
1066 ASSERT_EQ(0, system(cmd.c_str()));
1067
1068 unique_ptr<ItemJob> job(acc_.get(QString::fromStdString(full_path)));
1069 wait(job.get());
1070 EXPECT_TRUE(job->isValid());
1071
1072 auto file = job->item();
1073 auto old_etag = file.etag();
1074
1075 sleep(1); // Make sure mtime changes.
1076 cmd = string("touch ") + full_path;
1077 unique_ptr<Uploader> uploader(file.createUploader(Item::IgnoreConflict, 0));
1078 {
1079 QSignalSpy spy(uploader.get(), &Uploader::statusChanged);
1080 while (uploader->status() != Uploader::Ready)
1081 {
1082 ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME));
1083 }
1084 }
1085
1086 QSignalSpy spy(uploader.get(), &Uploader::statusChanged);
1087 uploader->close();
1088 while (uploader->status() != Uploader::Finished)
1089 {
1090 ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME));
1091 }
1092
1093 file = uploader->item();
1094 EXPECT_NE(old_etag, file.etag());
1095 EXPECT_EQ(int64_t(0), file.sizeInBytes());
1096}
1097
1098TEST_F(LocalProviderTest, update_close_too_soon)
1099{
1100 using namespace unity::storage::qt;
1101
1102 set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
1103
1104 auto full_path = ROOT_DIR + "/foo.txt";
1105 auto cmd = string("echo hello >") + full_path;
1106 ASSERT_EQ(0, system(cmd.c_str()));
1107
1108 unique_ptr<ItemJob> job(acc_.get(QString::fromStdString(full_path)));
1109 wait(job.get());
1110 EXPECT_TRUE(job->isValid());
1111
1112 auto file = job->item();
1113 auto old_etag = file.etag();
1114
1115 int const segments = 50;
1116 unique_ptr<Uploader> uploader(file.createUploader(Item::ErrorIfConflict, file_contents.size() * segments));
1117
1118 int count = 0;
1119 QTimer timer;
1120 timer.setSingleShot(false);
1121 timer.setInterval(10);
1122 QObject::connect(&timer, &QTimer::timeout, [&] {
1123 uploader->write(&file_contents[0], file_contents.size());
1124 count++;
1125 if (count == segments - 1)
1126 {
1127 uploader->close();
1128 }
1129 });
1130
1131 QSignalSpy spy(uploader.get(), &Uploader::statusChanged);
1132 timer.start();
1133 while (uploader->status() != Uploader::Error)
1134 {
1135 ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME));
1136 }
1137 ASSERT_EQ(Uploader::Error, uploader->status()) << uploader->error().errorString().toStdString();
1138 EXPECT_EQ("LogicError: finish() method called too early, size was given as 22300 but only 21854 bytes were received",
1139 uploader->error().errorString().toStdString());
1140}
1141
1142TEST_F(LocalProviderTest, update_write_too_much)
1143{
1144 using namespace unity::storage::qt;
1145
1146 set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
1147
1148 auto full_path = ROOT_DIR + "/foo.txt";
1149 auto cmd = string("echo hello >") + full_path;
1150 ASSERT_EQ(0, system(cmd.c_str()));
1151
1152 unique_ptr<ItemJob> job(acc_.get(QString::fromStdString(full_path)));
1153 wait(job.get());
1154 EXPECT_TRUE(job->isValid());
1155
1156 auto file = job->item();
1157 auto old_etag = file.etag();
1158
1159 int const segments = 50;
1160 // We write more than this many bytes below.
1161 unique_ptr<Uploader> uploader(file.createUploader(Item::ErrorIfConflict, file_contents.size() * segments - 1));
1162
1163 int count = 0;
1164 QTimer timer;
1165 timer.setSingleShot(false);
1166 timer.setInterval(10);
1167 QObject::connect(&timer, &QTimer::timeout, [&] {
1168 uploader->write(&file_contents[0], file_contents.size());
1169 count++;
1170 if (count == segments)
1171 {
1172 uploader->close();
1173 }
1174 });
1175
1176 QSignalSpy spy(uploader.get(), &Uploader::statusChanged);
1177 timer.start();
1178 while (uploader->status() != Uploader::Error)
1179 {
1180 ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME));
1181 }
1182 ASSERT_EQ(Uploader::Error, uploader->status());
1183 EXPECT_EQ("update(): received more than the expected number (22299) of bytes",
1184 uploader->error().message().toStdString());
1185}
1186
1187TEST_F(LocalProviderTest, upload_wrong_file_type)
1188{
1189 // We can't try an upload for a directory via the client API, so we use the LocalUploadJob directly.
1190
1191 auto p = make_shared<LocalProvider>();
1192
1193 string const dir = ROOT_DIR + "/dir";
1194 ASSERT_EQ(0, mkdir(dir.c_str(), 0755));
1195
1196 try
1197 {
1198 LocalUploadJob(p, dir, 0, "");
1199 FAIL();
1200 }
1201 catch (provider::InvalidArgumentException const& e)
1202 {
1203 EXPECT_EQ(string("InvalidArgumentException: update(): \"" + dir + "\" is not a file"), e.what());
1204 }
1205}
1206
1207TEST_F(LocalProviderTest, upload_root_noperm)
1208{
1209 // Force an error in prepare_channels when creating temp file.
1210
1211 auto p = make_shared<LocalProvider>();
1212
1213 ASSERT_EQ(0, chmod(ROOT_DIR.c_str(), 0644));
1214
1215 try
1216 {
1217 LocalUploadJob(p, ROOT_DIR, "name", 0, true);
1218 chmod(ROOT_DIR.c_str(), 0755);
1219 FAIL();
1220 }
1221 catch (provider::ResourceException const& e)
1222 {
1223 EXPECT_EQ(string("ResourceException: create_file(): cannot create temp file \"") + ROOT_DIR
1224 + "/.storage-framework-%%%%-%%%%-%%%%-%%%%\": Invalid argument",
1225 e.what());
1226 }
1227 chmod(ROOT_DIR.c_str(), 0755);
1228}
1229
1230TEST_F(LocalProviderTest, sanitize)
1231{
1232 // Force various errors in sanitize() for coverage.
1233
1234 auto p = make_shared<LocalProvider>();
1235
1236 try
1237 {
1238 LocalUploadJob(p, ROOT_DIR, "a/b", 0, true);
1239 FAIL();
1240 }
1241 catch (provider::InvalidArgumentException const& e)
1242 {
1243 EXPECT_STREQ("InvalidArgumentException: create_file(): name \"a/b\" cannot contain a slash", e.what());
1244 }
1245
1246 try
1247 {
1248 LocalUploadJob(p, ROOT_DIR, "..", 0, true);
1249 FAIL();
1250 }
1251 catch (provider::InvalidArgumentException const& e)
1252 {
1253 EXPECT_STREQ("InvalidArgumentException: create_file(): invalid name: \"..\"", e.what());
1254 }
1255
1256 try
1257 {
1258 LocalUploadJob(p, ROOT_DIR, ".storage-framework", 0, true);
1259 FAIL();
1260 }
1261 catch (provider::InvalidArgumentException const& e)
1262 {
1263 EXPECT_STREQ("InvalidArgumentException: create_file(): names beginning with \".storage-framework\" are reserved",
1264 e.what());
1265 }
1266}
1267
1268TEST_F(LocalProviderTest, throw_if_not_valid)
1269{
1270 // Make sure that we can't escape the root.
1271
1272 auto p = make_shared<LocalProvider>();
1273
1274 try
1275 {
1276 LocalUploadJob(p, ROOT_DIR + "/..", "a", 0, true);
1277 FAIL();
1278 }
1279 catch (provider::InvalidArgumentException const& e)
1280 {
1281 EXPECT_EQ(string("InvalidArgumentException: create_file(): invalid id: \"") + ROOT_DIR + "/..\"", e.what());
1282 }
1283
1284 try
1285 {
1286 LocalUploadJob(p, "/bin" , "a", 0, true);
1287 FAIL();
1288 }
1289 catch (provider::InvalidArgumentException const& e)
1290 {
1291 EXPECT_STREQ("InvalidArgumentException: create_file(): invalid id: \"/bin\"", e.what());
1292 }
1293}
1294
1295TEST_F(LocalProviderTest, create_file)
1296{
1297 using namespace unity::storage::qt;
1298
1299 set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
1300
1301 auto root = get_root(acc_);
1302 int const segments = 50;
1303 unique_ptr<Uploader> uploader(root.createFile("foo.txt", Item::ErrorIfConflict,
1304 file_contents.size() * segments, "text/plain"));
1305
1306 int count = 0;
1307 QTimer timer;
1308 timer.setSingleShot(false);
1309 timer.setInterval(10);
1310 QObject::connect(&timer, &QTimer::timeout, [&] {
1311 uploader->write(&file_contents[0], file_contents.size());
1312 count++;
1313 if (count == segments)
1314 {
1315 uploader->close();
1316 }
1317 });
1318
1319 QSignalSpy spy(uploader.get(), &Uploader::statusChanged);
1320 while (uploader->status() != Uploader::Ready)
1321 {
1322 ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME));
1323 }
1324 timer.start();
1325 while (uploader->status() != Uploader::Finished)
1326 {
1327 ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME));
1328 }
1329
1330 auto file = uploader->item();
1331 EXPECT_EQ(int64_t(file_contents.size() * segments), file.sizeInBytes());
1332}
1333
1334TEST_F(LocalProviderTest, create_file_ignore_conflict)
1335{
1336 using namespace unity::storage::qt;
1337
1338 set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
1339
1340 auto full_path = ROOT_DIR + "/foo.txt";
1341 auto cmd = string("echo hello >") + full_path;
1342 ASSERT_EQ(0, system(cmd.c_str()));
1343
1344 auto root = get_root(acc_);
1345 int const segments = 50;
1346 unique_ptr<Uploader> uploader(root.createFile("foo.txt", Item::IgnoreConflict,
1347 file_contents.size() * segments, "text/plain"));
1348
1349 int count = 0;
1350 QTimer timer;
1351 timer.setSingleShot(false);
1352 timer.setInterval(10);
1353 QObject::connect(&timer, &QTimer::timeout, [&] {
1354 uploader->write(&file_contents[0], file_contents.size());
1355 count++;
1356 if (count == segments)
1357 {
1358 uploader->close();
1359 }
1360 });
1361
1362 QSignalSpy spy(uploader.get(), &Uploader::statusChanged);
1363 while (uploader->status() != Uploader::Ready)
1364 {
1365 ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME));
1366 }
1367 timer.start();
1368 while (uploader->status() != Uploader::Finished)
1369 {
1370 ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME));
1371 }
1372
1373 auto file = uploader->item();
1374 EXPECT_EQ(int64_t(file_contents.size() * segments), file.sizeInBytes());
1375}
1376
1377TEST_F(LocalProviderTest, create_file_error_if_conflict)
1378{
1379 using namespace unity::storage::qt;
1380
1381 set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
1382
1383 auto full_path = ROOT_DIR + "/foo.txt";
1384 auto cmd = string("echo hello >") + full_path;
1385 ASSERT_EQ(0, system(cmd.c_str()));
1386
1387 auto root = get_root(acc_);
1388 int const segments = 50;
1389 unique_ptr<Uploader> uploader(root.createFile("foo.txt", Item::ErrorIfConflict,
1390 file_contents.size() * segments, "text/plain"));
1391
1392 QSignalSpy spy(uploader.get(), &Uploader::statusChanged);
1393 while (uploader->status() != Uploader::Error)
1394 {
1395 ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME));
1396 }
1397 EXPECT_EQ(string("create_file(): \"") + full_path + "\" exists already",
1398 uploader->error().message().toStdString());
1399}
1400
1401TEST_F(LocalProviderTest, create_file_created_during_upload)
1402{
1403 using namespace unity::storage::qt;
1404
1405 set_provider(unique_ptr<provider::ProviderBase>(new LocalProvider));
1406
1407 auto root = get_root(acc_);
1408 int const segments = 50;
1409 string full_path = ROOT_DIR + "/foo.txt";
1410 unique_ptr<Uploader> uploader(root.createFile("foo.txt", Item::ErrorIfConflict,
1411 file_contents.size() * segments, "text/plain"));
1412
1413 int count = 0;
1414 QTimer timer;
1415 timer.setSingleShot(false);
1416 timer.setInterval(10);
1417 QObject::connect(&timer, &QTimer::timeout, [&] {
1418 uploader->write(&file_contents[0], file_contents.size());
1419 count++;
1420 if (count == segments / 2)
1421 {
1422 string cmd = "touch " + full_path;
1423 ASSERT_EQ(0, system(cmd.c_str()));
1424 }
1425 else if (count == segments)
1426 {
1427 uploader->close();
1428 }
1429 });
1430
1431 QSignalSpy spy(uploader.get(), &Uploader::statusChanged);
1432 while (uploader->status() != Uploader::Ready)
1433 {
1434 ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME));
1435 }
1436 timer.start();
1437 while (uploader->status() != Uploader::Error)
1438 {
1439 ASSERT_TRUE(spy.wait(SIGNAL_WAIT_TIME));
1440 }
1441 EXPECT_EQ(string("create_file(): \"") + full_path + "\" exists already",
1442 uploader->error().message().toStdString());
1443}
1444
1445int main(int argc, char** argv)
1446{
1447 setenv("LANG", "C", true);
1448 setenv("SF_LOCAL_PROVIDER_ROOT", ROOT_DIR.c_str(), true);
1449
1450 // Test test fixture repeatedly creates and tears down the dbus connection.
1451 // The provider calls g_file_new_for_path() which talks to the GVfs backend
1452 // via dbus. If the dbus connection disappears, that causes GIO to send a
1453 // a SIGTERM, killing the test.
1454 // Setting GIO_USE_VFS variable to "local" disables sending the signal.
1455 setenv("GIO_USE_VFS", "local", true);
1456
1457 QCoreApplication app(argc, argv);
1458
1459 ::testing::InitGoogleTest(&argc, argv);
1460 int rc = RUN_ALL_TESTS();
1461
1462 // Process any pending events to avoid bogus leak reports from valgrind.
1463 QCoreApplication::sendPostedEvents();
1464 QCoreApplication::processEvents();
1465
1466 if (rc == 0)
1467 {
1468 boost::system::error_code ec;
1469 boost::filesystem::remove_all(ROOT_DIR, ec);
1470 }
1471
1472 return rc;
1473}
01474
=== added file 'tests/utils/env_var_guard.h'
--- tests/utils/env_var_guard.h 1970-01-01 00:00:00 +0000
+++ tests/utils/env_var_guard.h 2017-03-17 05:04:57 +0000
@@ -0,0 +1,70 @@
1/*
2 * Copyright (C) 2015 Canonical Ltd.
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License version 3 as
6 * published by the Free Software Foundation.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 *
16 * Authored by: Michi Henning <michi.henning@canonical.com>
17 */
18
19#pragma once
20
21#include <cassert>
22#include <cstdlib>
23#include <string>
24
25class EnvVarGuard final
26{
27public:
28 // Set environment variable 'name' to 'val'.
29 // To clear the variable, pass nullptr for 'val'.
30 // The destructor restores the original setting.
31 EnvVarGuard(char const* name, char const* val)
32 : name_(name)
33 {
34 assert(name && *name != '\0');
35 auto const old_val = getenv(name);
36 if ((was_set_ = old_val != nullptr))
37 {
38 old_value_ = old_val;
39 }
40 if (val)
41 {
42 setenv(name, val, true);
43 }
44 else
45 {
46 unsetenv(name);
47 }
48 }
49
50 // Restore the original setting.
51 ~EnvVarGuard()
52 {
53 if (was_set_)
54 {
55 setenv(name_.c_str(), old_value_.c_str(), true);
56 }
57 else
58 {
59 unsetenv(name_.c_str());
60 }
61 }
62
63 EnvVarGuard(const EnvVarGuard&) = delete;
64 EnvVarGuard& operator=(const EnvVarGuard&) = delete;
65
66private:
67 std::string name_;
68 std::string old_value_;
69 bool was_set_;
70};

Subscribers

People subscribed via source and target branches

to all changes: