openvfs-rb 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4e4e0c990813542ab1b0b2aee640ffe25e13b865ec89300f22e796a37251f37f
4
+ data.tar.gz: 618bec06ee55f463316af80cb2cfad9e02b21a66748629d070d087c6dc2a645f
5
+ SHA512:
6
+ metadata.gz: 11ec8556f9b9e44533591404eee4ffc900b2a615918972f9d8be968741ec13123a8de10d2cf49509ed98cf35b82cb8beda73c675867926666e6076401537e803
7
+ data.tar.gz: 9988ae322baa3fbd3a71edabe1db176f025172fb2ff4477714df88bff8942e4e6f2bc4bf1cbae14cf4ba25c686208482fc312346b753d632ec1af3fd557f1bb3
data/README.md ADDED
@@ -0,0 +1,97 @@
1
+ # openvfs-rb
2
+
3
+ A virtual filesystem for Ruby.
4
+
5
+ ```ruby
6
+ gem "openvfs-rb"
7
+ ```
8
+
9
+ ```ruby
10
+ require "openvfs"
11
+
12
+ OpenVFS::VERSION
13
+ ```
14
+
15
+ ## OpenVFS API
16
+
17
+ The native extension wraps OpenVFS's registration and placeholder metadata
18
+ types:
19
+
20
+ ```ruby
21
+ registration = OpenVFS::Registration.register_filesystem(path, owner)
22
+ raise registration.error unless registration.ok?
23
+
24
+ attributes = OpenVFS::PlaceHolderAttributes.create(path, etag, file_id, size)
25
+ attributes.state = OpenVFS::Constants::States::DeHydrated
26
+ serialized_attributes = attributes.to_data
27
+ ```
28
+
29
+ OpenVFS development headers and `libopenvfs` are required to build the
30
+ extension. In this repository's Nix development shell they are provided by the
31
+ locked OpenVFS source input.
32
+
33
+ ## OpenCloud Desktop sync client
34
+
35
+ `OpenVFS` wraps the desktop client's account and keychain. Configure a folder, store the account's credential, then run the client:
36
+
37
+ ```ruby
38
+ require "openvfs/occ"
39
+
40
+ uuid = OpenVFS.configure(
41
+ "/local/folder",
42
+ url: "https://cloud.example",
43
+ remote_path: "/remote.php/dav/files/user",
44
+ )
45
+
46
+ account = OpenVFS::Account.create(uuid)
47
+ account.url = "https://cloud.example"
48
+ account.credential_manager.set("http/oauthtoken", ENV.fetch("OPENCLOUD_ACCESS_TOKEN"))
49
+
50
+ OpenVFS.start(["--logfile", "-"])
51
+ ```
52
+
53
+ `OpenVFS.configure` writes `~/.config/openvfs-rb/openvfs-rb.cfg`; it does not touch the keychain. `OpenVFS::Account.create` returns the wrapped account, `account.url=` sets the server, and `account.credential_manager` exposes `set`, `get`, `remove` and `contains?`. The keychain methods block until QtKeychain's job finishes, so no event loop needs to be running. For a server that enables HTTP Basic authentication, store the complete `Basic ...` header instead of a bearer token. `OpenVFS.start` calls the desktop's `OCC::init`, which runs the sync client's event loop and does not return until the client exits; run it in its own process.
54
+
55
+ ### Building the desktop subset
56
+
57
+ The locked `desktop` flake input is built into a small static library with Qt's generated meta-object code, using the patches under `patches/`. The patches keep the sync engine and keychain, and omit the desktop UI, QML, translations and unrelated plugins.
58
+
59
+ ```sh
60
+ nix develop --command ruby -C ext/openvfs extconf.rb
61
+ nix develop --command make -C ext/openvfs
62
+ nix develop --command ruby -C ext/openvfs/occ extconf.rb
63
+ nix develop --command make -C ext/openvfs/occ
64
+ nix develop --command bundle exec sus
65
+ ```
66
+
67
+ The development shell provides `OPENCLOUD_UPLOAD_PREFIX` and `pkg-config`
68
+ metadata. Outside the shell, install that subset and its Qt 6 development
69
+ dependencies, then make `opencloud-upload.pc` visible to `pkg-config`.
70
+ The installed `opencloud-upload.pc`, headers and static library are what the extension builds against.
71
+
72
+ ### Local OpenCloud server
73
+
74
+ The integration test in `test/openvfs/sync.rb` configures a temporary local folder, starts the client, and checks that a file dropped into the folder reaches a real OpenCloud server.
75
+
76
+ The test skips while no server answers `OPENCLOUD_URL`, so the default run stays hermetic.
77
+
78
+ ```sh
79
+ docker compose up -d opencloud
80
+ nix develop --command bundle exec sus test/openvfs/sync.rb
81
+ ```
82
+
83
+ The compose file starts `opencloudeu/opencloud-rolling:9.0.0` with WebDAV basic
84
+ authentication enabled and listens on `127.0.0.1:${OPENCLOUD_PORT:-18280}`.
85
+ Override the credentials with `OPENCLOUD_USER` and `OPENCLOUD_PASSWORD` when the default admin user is not the account you want to sync.
86
+
87
+ ## Contributing
88
+
89
+ See [RELEASE.md](RELEASE.md) for the release process and
90
+ [DEPRECATIONS.md](DEPRECATIONS.md) for the deprecation policy.
91
+
92
+ ## License
93
+
94
+ Released under the [MIT License](license.md).
95
+
96
+ The linked OpenCloud Desktop sources retain their upstream GPL/LGPL licenses;
97
+ the subset derivation installs the upstream `COPYING` file with the library.
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mkmf-rice"
4
+
5
+ prefix = ENV.fetch("OPENVFS_PREFIX") do
6
+ abort "OPENVFS_PREFIX must point to the OpenVFS installation prefix"
7
+ end
8
+
9
+ openvfs_include_dir = File.join(prefix, "include")
10
+ openvfs_library_dir = File.join(prefix, "lib")
11
+
12
+ dir_config("openvfs", openvfs_include_dir, openvfs_library_dir)
13
+ $CXXFLAGS << " -std=c++23 -Wno-array-bounds -ftemplate-backtrace-limit=0"
14
+ $LDFLAGS << " -Wl,-rpath,#{openvfs_library_dir}"
15
+
16
+ unless have_header("openvfs/openvfs.h")
17
+ abort "missing OpenVFS headers"
18
+ end
19
+
20
+ unless have_library("openvfs")
21
+ abort "missing libopenvfs"
22
+ end
23
+
24
+ create_makefile "openvfs/openvfs"
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mkmf-rice"
4
+
5
+ if ENV.key?("OPENCLOUD_UPLOAD_PREFIX")
6
+ pkg_config_path = File.join(ENV.fetch("OPENCLOUD_UPLOAD_PREFIX"), "lib", "pkgconfig")
7
+ ENV["PKG_CONFIG_PATH"] = [pkg_config_path, ENV["PKG_CONFIG_PATH"]].compact.join(File::PATH_SEPARATOR)
8
+ end
9
+
10
+ unless pkg_config("opencloud-upload")
11
+ abort "missing opencloud-upload development files; enter nix develop or set OPENCLOUD_UPLOAD_PREFIX"
12
+ end
13
+
14
+ $CXXFLAGS << " -std=c++23 -fPIC -Wno-array-bounds"
15
+
16
+ # The static library is built with -ffunction-sections, so the link can drop every function the extension never calls. ld64 spells that -dead_strip and has no equivalent of --exclude-libs.
17
+ if RbConfig::CONFIG["host_os"].match?(/darwin/)
18
+ $LDFLAGS << " -Wl,-dead_strip"
19
+ else
20
+ $LDFLAGS << " -Wl,--gc-sections -Wl,--exclude-libs,ALL"
21
+ end
22
+
23
+ # opencloudsynclib.h rather than a real header: mkmf puts ruby.h first in the program it compiles to probe, and on Windows that defines truncate and access as macros, which Qt refuses to be parsed after. The generated export header pulls in nothing, so it answers the only question being asked - whether the development files are installed.
24
+ unless have_header("libsync/opencloudsynclib.h")
25
+ abort "missing OpenCloud Desktop upload headers"
26
+ end
27
+
28
+ create_makefile "openvfs/occ/occ"
@@ -0,0 +1,502 @@
1
+ // Rice bindings for OpenCloud Desktop.
2
+ //
3
+ // Only two classes are wrapped: OCC::Account and OCC::CredentialManager, plus the types
4
+ // their methods take and return, and OCC::init - the desktop's own entry point, with the
5
+ // UI patched out. Nothing here adds behaviour that libsync does not already have.
6
+ //
7
+ // is_convertible returns a double score, matching Rice 4.12.0's own converters in
8
+ // stl.hpp; Rice's published docs still describe an older enum form.
9
+
10
+ // Qt comes before Ruby, and the order is load-bearing on Windows: ruby.h defines truncate and access as macros onto its own win32 replacements, and Qt's headers stop with an #error rather than be parsed after that.
11
+ #include <libsync/account.h>
12
+ #include <libsync/creds/credentialmanager.h>
13
+
14
+ #include <qt6keychain/keychain.h>
15
+
16
+ #include <QCoreApplication>
17
+ #include <QEventLoop>
18
+ #include <QString>
19
+ #include <QStringList>
20
+ #include <QUrl>
21
+ #include <QTimer>
22
+ #include <QUuid>
23
+ #include <QVariant>
24
+
25
+ // rice.hpp uses std::exception_ptr, std::sort and std::count_if without including the headers that define them. libstdc++ happens to pull them in anyway; libc++ does not, so on clang exception_ptr is incomplete and the algorithms are missing unless these come first.
26
+ #include <algorithm>
27
+ #include <exception>
28
+
29
+ #include <rice/rice.hpp>
30
+ #include <rice/stl.hpp>
31
+
32
+ #include <memory>
33
+
34
+ namespace OCC {
35
+ // Defined in src/gui/main.cpp, which the patches reduce to the non-UI bootstrap.
36
+ int init(const QStringList &arguments);
37
+ }
38
+
39
+ // ---------------------------------------------------------------------------------------------
40
+ // Type converters: Rice ships these for fundamental types and the STL, so Qt's own need
41
+ // the three documented steps - specialize Type, To_Ruby and From_Ruby.
42
+ // ---------------------------------------------------------------------------------------------
43
+
44
+ namespace Rice::detail
45
+ {
46
+ template<>
47
+ struct Type<QString>
48
+ {
49
+ static bool verify() { return true; }
50
+ };
51
+
52
+ template<>
53
+ class To_Ruby<QString>
54
+ {
55
+ public:
56
+ To_Ruby() = default;
57
+ explicit To_Ruby(Arg* arg) : arg_(arg) { }
58
+
59
+ VALUE convert(const QString& string)
60
+ {
61
+ const QByteArray utf8 = string.toUtf8();
62
+ return String(std::string(utf8.constData(), utf8.size())).value();
63
+ }
64
+
65
+ private:
66
+ Arg* arg_ = nullptr;
67
+ };
68
+
69
+ template<>
70
+ class From_Ruby<QString>
71
+ {
72
+ public:
73
+ From_Ruby() = default;
74
+ explicit From_Ruby(Arg* arg) : arg_(arg) { }
75
+
76
+ double is_convertible(VALUE value)
77
+ {
78
+ return rb_type(value) == RUBY_T_STRING ? Convertible::Exact : Convertible::None;
79
+ }
80
+
81
+ QString convert(VALUE value)
82
+ {
83
+ const std::string string = From_Ruby<std::string>().convert(value);
84
+ return QString::fromUtf8(string.data(), static_cast<qsizetype>(string.size()));
85
+ }
86
+
87
+ private:
88
+ Arg* arg_ = nullptr;
89
+ };
90
+
91
+ template<>
92
+ struct Type<QStringList>
93
+ {
94
+ static bool verify() { return Type<QString>::verify(); }
95
+ };
96
+
97
+ template<>
98
+ class To_Ruby<QStringList>
99
+ {
100
+ public:
101
+ To_Ruby() = default;
102
+ explicit To_Ruby(Arg* arg) : arg_(arg) { }
103
+
104
+ VALUE convert(const QStringList& strings)
105
+ {
106
+ Array result;
107
+ for (const QString& string : strings) {
108
+ result.push(string, true);
109
+ }
110
+ return result.value();
111
+ }
112
+
113
+ private:
114
+ Arg* arg_ = nullptr;
115
+ };
116
+
117
+ template<>
118
+ class From_Ruby<QStringList>
119
+ {
120
+ public:
121
+ From_Ruby() = default;
122
+ explicit From_Ruby(Arg* arg) : arg_(arg) { }
123
+
124
+ double is_convertible(VALUE value)
125
+ {
126
+ return rb_type(value) == RUBY_T_ARRAY ? Convertible::Exact : Convertible::None;
127
+ }
128
+
129
+ QStringList convert(VALUE value)
130
+ {
131
+ Array array(value);
132
+ QStringList result;
133
+ for (long i = 0; i < array.size(); i++) {
134
+ result.append(From_Ruby<QString>().convert(array[i].value()));
135
+ }
136
+ return result;
137
+ }
138
+
139
+ private:
140
+ Arg* arg_ = nullptr;
141
+ };
142
+
143
+ template<>
144
+ struct Type<QUrl>
145
+ {
146
+ static bool verify() { return true; }
147
+ };
148
+
149
+ template<>
150
+ class To_Ruby<QUrl>
151
+ {
152
+ public:
153
+ To_Ruby() = default;
154
+ explicit To_Ruby(Arg* arg) : arg_(arg) { }
155
+
156
+ VALUE convert(const QUrl& url) { return To_Ruby<QString>().convert(url.toString()); }
157
+
158
+ private:
159
+ Arg* arg_ = nullptr;
160
+ };
161
+
162
+ template<>
163
+ class From_Ruby<QUrl>
164
+ {
165
+ public:
166
+ From_Ruby() = default;
167
+ explicit From_Ruby(Arg* arg) : arg_(arg) { }
168
+
169
+ double is_convertible(VALUE value)
170
+ {
171
+ return rb_type(value) == RUBY_T_STRING ? Convertible::Exact : Convertible::None;
172
+ }
173
+
174
+ QUrl convert(VALUE value) { return QUrl(From_Ruby<QString>().convert(value)); }
175
+
176
+ private:
177
+ Arg* arg_ = nullptr;
178
+ };
179
+
180
+ template<>
181
+ struct Type<QUuid>
182
+ {
183
+ static bool verify() { return true; }
184
+ };
185
+
186
+ template<>
187
+ class To_Ruby<QUuid>
188
+ {
189
+ public:
190
+ To_Ruby() = default;
191
+ explicit To_Ruby(Arg* arg) : arg_(arg) { }
192
+
193
+ VALUE convert(const QUuid& uuid)
194
+ {
195
+ return To_Ruby<QString>().convert(uuid.toString(QUuid::WithoutBraces));
196
+ }
197
+
198
+ private:
199
+ Arg* arg_ = nullptr;
200
+ };
201
+
202
+ template<>
203
+ class From_Ruby<QUuid>
204
+ {
205
+ public:
206
+ From_Ruby() = default;
207
+ explicit From_Ruby(Arg* arg) : arg_(arg) { }
208
+
209
+ double is_convertible(VALUE value)
210
+ {
211
+ return rb_type(value) == RUBY_T_STRING ? Convertible::Exact : Convertible::None;
212
+ }
213
+
214
+ QUuid convert(VALUE value) { return QUuid::fromString(From_Ruby<QString>().convert(value)); }
215
+
216
+ private:
217
+ Arg* arg_ = nullptr;
218
+ };
219
+
220
+ // CredentialManager::set stores a QVariant and CredentialJob::data returns one.
221
+ template<>
222
+ struct Type<QVariant>
223
+ {
224
+ static bool verify() { return true; }
225
+ };
226
+
227
+ template<>
228
+ class To_Ruby<QVariant>
229
+ {
230
+ public:
231
+ To_Ruby() = default;
232
+ explicit To_Ruby(Arg* arg) : arg_(arg) { }
233
+
234
+ VALUE convert(const QVariant& variant)
235
+ {
236
+ if (!variant.isValid()) {
237
+ return Qnil;
238
+ }
239
+ switch (variant.typeId()) {
240
+ case QMetaType::Bool:
241
+ return variant.toBool() ? Qtrue : Qfalse;
242
+ case QMetaType::Int:
243
+ case QMetaType::UInt:
244
+ case QMetaType::LongLong:
245
+ case QMetaType::ULongLong:
246
+ return To_Ruby<long long>().convert(variant.toLongLong());
247
+ case QMetaType::Double:
248
+ return To_Ruby<double>().convert(variant.toDouble());
249
+ default:
250
+ return To_Ruby<QString>().convert(variant.toString());
251
+ }
252
+ }
253
+
254
+ private:
255
+ Arg* arg_ = nullptr;
256
+ };
257
+
258
+ template<>
259
+ class From_Ruby<QVariant>
260
+ {
261
+ public:
262
+ From_Ruby() = default;
263
+ explicit From_Ruby(Arg* arg) : arg_(arg) { }
264
+
265
+ double is_convertible(VALUE value)
266
+ {
267
+ switch (rb_type(value)) {
268
+ case RUBY_T_STRING:
269
+ case RUBY_T_FIXNUM:
270
+ case RUBY_T_BIGNUM:
271
+ case RUBY_T_FLOAT:
272
+ case RUBY_T_TRUE:
273
+ case RUBY_T_FALSE:
274
+ case RUBY_T_NIL:
275
+ return Convertible::Exact;
276
+ default:
277
+ return Convertible::None;
278
+ }
279
+ }
280
+
281
+ QVariant convert(VALUE value)
282
+ {
283
+ switch (rb_type(value)) {
284
+ case RUBY_T_NIL:
285
+ return QVariant();
286
+ case RUBY_T_TRUE:
287
+ return QVariant(true);
288
+ case RUBY_T_FALSE:
289
+ return QVariant(false);
290
+ case RUBY_T_FIXNUM:
291
+ case RUBY_T_BIGNUM:
292
+ return QVariant(From_Ruby<long long>().convert(value));
293
+ case RUBY_T_FLOAT:
294
+ return QVariant(From_Ruby<double>().convert(value));
295
+ default:
296
+ return QVariant(From_Ruby<QString>().convert(value));
297
+ }
298
+ }
299
+
300
+ private:
301
+ Arg* arg_ = nullptr;
302
+ };
303
+
304
+ // Rice matches converters on the exact parameter type, so `const T&` parameters need
305
+ // their own; each stores the converted value and returns a reference to it.
306
+ template<typename T>
307
+ class From_Ruby_Qt_Ref
308
+ {
309
+ public:
310
+ From_Ruby_Qt_Ref() = default;
311
+ explicit From_Ruby_Qt_Ref(Arg* arg) : arg_(arg) { }
312
+
313
+ double is_convertible(VALUE value) { return From_Ruby<T>(this->arg_).is_convertible(value); }
314
+
315
+ T& convert(VALUE value)
316
+ {
317
+ this->converted_ = From_Ruby<T>(this->arg_).convert(value);
318
+ return this->converted_;
319
+ }
320
+
321
+ private:
322
+ Arg* arg_ = nullptr;
323
+ T converted_;
324
+ };
325
+
326
+ template<>
327
+ class From_Ruby<QString&> : public From_Ruby_Qt_Ref<QString>
328
+ {
329
+ public:
330
+ using From_Ruby_Qt_Ref<QString>::From_Ruby_Qt_Ref;
331
+ };
332
+
333
+ template<>
334
+ class From_Ruby<QUrl&> : public From_Ruby_Qt_Ref<QUrl>
335
+ {
336
+ public:
337
+ using From_Ruby_Qt_Ref<QUrl>::From_Ruby_Qt_Ref;
338
+ };
339
+
340
+ template<>
341
+ class From_Ruby<QUuid&> : public From_Ruby_Qt_Ref<QUuid>
342
+ {
343
+ public:
344
+ using From_Ruby_Qt_Ref<QUuid>::From_Ruby_Qt_Ref;
345
+ };
346
+
347
+ template<>
348
+ class From_Ruby<QVariant&> : public From_Ruby_Qt_Ref<QVariant>
349
+ {
350
+ public:
351
+ using From_Ruby_Qt_Ref<QVariant>::From_Ruby_Qt_Ref;
352
+ };
353
+
354
+ template<>
355
+ class From_Ruby<QStringList&> : public From_Ruby_Qt_Ref<QStringList>
356
+ {
357
+ public:
358
+ using From_Ruby_Qt_Ref<QStringList>::From_Ruby_Qt_Ref;
359
+ };
360
+ }
361
+
362
+ using namespace Rice;
363
+
364
+ namespace {
365
+ constexpr int keychainTimeoutMsC = 30000;
366
+
367
+ // QtKeychain queues its work onto the Qt event loop. Before OpenVFS.start there is no
368
+ // loop, so a credential operation creates a temporary application, runs the job to
369
+ // completion, and tears the application down again.
370
+ class ApplicationScope
371
+ {
372
+ public:
373
+ ApplicationScope()
374
+ {
375
+ if (QCoreApplication::instance()) {
376
+ return;
377
+ }
378
+
379
+ static int argc = 1;
380
+ static char name[] = "openvfs-rb";
381
+ static char *argv[] = {name, nullptr};
382
+ _application = std::make_unique<QCoreApplication>(argc, argv);
383
+ _application->setApplicationName(QStringLiteral("openvfs-rb"));
384
+ }
385
+
386
+ ~ApplicationScope()
387
+ {
388
+ if (!_application) {
389
+ return;
390
+ }
391
+
392
+ _application.reset();
393
+ // QCoreApplication clears the application name when it is destroyed.
394
+ QCoreApplication::setApplicationName(QStringLiteral("openvfs-rb"));
395
+ }
396
+
397
+ private:
398
+ std::unique_ptr<QCoreApplication> _application;
399
+ };
400
+
401
+ bool wait_for(QKeychain::Job *job)
402
+ {
403
+ QEventLoop loop;
404
+ QObject::connect(job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
405
+ QTimer::singleShot(keychainTimeoutMsC, &loop, &QEventLoop::quit);
406
+ loop.exec();
407
+
408
+ return job->error() == QKeychain::NoError;
409
+ }
410
+
411
+ bool credential_set(OCC::CredentialManager &manager, const QString &key, const QVariant &data)
412
+ {
413
+ if (!data.isValid()) {
414
+ return false;
415
+ }
416
+
417
+ ApplicationScope application;
418
+ return wait_for(manager.set(key, data));
419
+ }
420
+
421
+ bool credential_remove(OCC::CredentialManager &manager, const QString &key)
422
+ {
423
+ ApplicationScope application;
424
+ if (!manager.contains(key)) {
425
+ return false;
426
+ }
427
+
428
+ return wait_for(manager.remove(key));
429
+ }
430
+
431
+ bool credential_contains(const OCC::CredentialManager &manager, const QString &key)
432
+ {
433
+ ApplicationScope application;
434
+ return manager.contains(key);
435
+ }
436
+
437
+ QVariant credential_get(OCC::CredentialManager &manager, const QString &key)
438
+ {
439
+ ApplicationScope application;
440
+ auto *job = manager.get(key);
441
+
442
+ QEventLoop loop;
443
+ QObject::connect(job, &OCC::CredentialJob::finished, &loop, &QEventLoop::quit);
444
+ QTimer::singleShot(keychainTimeoutMsC, &loop, &QEventLoop::quit);
445
+ loop.exec();
446
+
447
+ if (job->error() != QKeychain::NoError) {
448
+ return {};
449
+ }
450
+
451
+ return job->data();
452
+ }
453
+ }
454
+
455
+ extern "C" void Init_occ()
456
+ {
457
+ Rice::detail::cpp_protect([] {
458
+ QCoreApplication::setApplicationName(QStringLiteral("openvfs-rb"));
459
+
460
+ Module rb_mOpenVFS = define_module("OpenVFS");
461
+
462
+ // -----------------------------------------------------------------------------------------
463
+ // The desktop's entry point: src/gui/main.cpp, reduced by the patches to its non-UI
464
+ // bootstrap. It takes the arguments the command line already accepts.
465
+ // -----------------------------------------------------------------------------------------
466
+
467
+ rb_mOpenVFS.define_module_function(
468
+ "start",
469
+ [](const QStringList &arguments) {
470
+ return OCC::init(QStringList{QStringLiteral("openvfs-rb")} + arguments);
471
+ },
472
+ Arg("arguments") = QStringList{});
473
+
474
+ // -----------------------------------------------------------------------------------------
475
+ // OCC::CredentialManager - the account's keychain. Each operation runs the keychain job
476
+ // to completion, so Ruby never sees a Qt job object.
477
+ // -----------------------------------------------------------------------------------------
478
+
479
+ Data_Type<OCC::CredentialManager> rb_cCredentialManager =
480
+ define_class_under<OCC::CredentialManager>(rb_mOpenVFS, "CredentialManager")
481
+ .define_method("set", &credential_set, Arg("key"), Arg("data"))
482
+ .define_method("get", &credential_get, Arg("key"))
483
+ .define_method("remove", &credential_remove, Arg("key"))
484
+ .define_method("contains?", &credential_contains, Arg("key"));
485
+
486
+ // -----------------------------------------------------------------------------------------
487
+ // OCC::Account - one configured server connection.
488
+ // -----------------------------------------------------------------------------------------
489
+
490
+ // Account::create hands back a QSharedPointer, and every libsync class takes one.
491
+ // Wrapping the pointer itself - rather than what it points at - is what keeps the
492
+ // reference count alive while Ruby holds it.
493
+ Data_Type<OCC::AccountPtr> rb_cAccount =
494
+ define_class_under<OCC::AccountPtr>(rb_mOpenVFS, "Account")
495
+ .define_singleton_function("create", &OCC::Account::create, Arg("uuid"))
496
+ .define_method("uuid", [](const OCC::AccountPtr &self) { return self->uuid(); })
497
+ .define_method("url", [](const OCC::AccountPtr &self) { return self->url(); })
498
+ .define_method("url=", [](OCC::AccountPtr &self, const QUrl &url) { self->setUrl(url); }, Arg("url"))
499
+ .define_method("credential_manager", [](const OCC::AccountPtr &self) { return self->credentialManager(); },
500
+ Return().keepAlive());
501
+ });
502
+ }
@@ -0,0 +1,113 @@
1
+ // rice.hpp uses std::exception_ptr, std::sort and std::count_if without including the headers that define them. libstdc++ happens to pull them in anyway; libc++ does not, so on clang exception_ptr is incomplete and the algorithms are missing unless these come first.
2
+ #include <algorithm>
3
+ #include <exception>
4
+
5
+ #include <rice/rice.hpp>
6
+ #include <rice/stl.hpp>
7
+
8
+ #include <openvfs/openvfs.h>
9
+ #include <openvfs/openvfconstants.h>
10
+
11
+ #include <cstdint>
12
+ #include <filesystem>
13
+ #include <optional>
14
+ #include <string>
15
+ #include <vector>
16
+
17
+ namespace {
18
+
19
+ using OpenVFS::PlaceHolderAttributes;
20
+ using OpenVFS::Registration;
21
+ using OpenVFS::Constants::PinStates;
22
+ using OpenVFS::Constants::States;
23
+ using Rice::Data_Type;
24
+ using Rice::Enum;
25
+ using Rice::Module;
26
+
27
+ std::filesystem::path path_from_string(const std::string &path)
28
+ {
29
+ return std::filesystem::path(path);
30
+ }
31
+
32
+ std::string path_to_string(const std::filesystem::path &path)
33
+ {
34
+ return path.string();
35
+ }
36
+
37
+ std::vector<uint8_t> bytes_from_string(const std::string &data)
38
+ {
39
+ return {data.begin(), data.end()};
40
+ }
41
+
42
+ std::string string_from_bytes(const std::vector<uint8_t> &data)
43
+ {
44
+ if (data.empty()) {
45
+ return {};
46
+ }
47
+ return {reinterpret_cast<const char *>(data.data()), data.size()};
48
+ }
49
+
50
+ }
51
+
52
+ extern "C" void Init_openvfs()
53
+ {
54
+ Module rb_mOpenVFS = Rice::define_module("OpenVFS");
55
+ Module rb_mConstants = Rice::define_module_under(rb_mOpenVFS, "Constants");
56
+
57
+ Enum<States> rb_eStates = Rice::define_enum_under<States>("States", rb_mConstants)
58
+ .define_value("Hydrated", States::Hydrated)
59
+ .define_value("DeHydrated", States::DeHydrated)
60
+ .define_value("Hydrating", States::Hydrating);
61
+
62
+ Enum<PinStates> rb_ePinStates = Rice::define_enum_under<PinStates>("PinStates", rb_mConstants)
63
+ .define_value("Unspecified", PinStates::Unspecified)
64
+ .define_value("OnlineOnly", PinStates::OnlineOnly)
65
+ .define_value("AlwaysLocal", PinStates::AlwaysLocal)
66
+ .define_value("Inherited", PinStates::Inherited)
67
+ .define_value("Excluded", PinStates::Excluded);
68
+
69
+ Module rb_mXAttributeNames = Rice::define_module_under(rb_mConstants, "XAttributeNames");
70
+ rb_mXAttributeNames.define_constant("Data", std::string(OpenVFS::Constants::XAttributeNames::Data));
71
+ rb_mXAttributeNames.define_constant("RealSize", std::string(OpenVFS::Constants::XAttributeNames::RealSize));
72
+ rb_mXAttributeNames.define_constant("Registration", std::string(OpenVFS::Constants::XAttributeNames::Registration));
73
+
74
+ Data_Type<Registration> rb_cRegistration = Rice::define_class_under<Registration>(rb_mOpenVFS, "Registration")
75
+ .define_singleton_function("register_filesystem", [](const std::string &path, const std::string &owner) {
76
+ return Registration::registerFilesystem(path_from_string(path), owner);
77
+ })
78
+ .define_singleton_function("from_attributes", [](const std::string &path) {
79
+ return Registration::fromAttributes(path_from_string(path));
80
+ })
81
+ .define_singleton_function("from_data", [](const std::string &path, const std::string &data) {
82
+ return Registration::fromData(path_from_string(path), bytes_from_string(data));
83
+ })
84
+ .define_singleton_function("unregister_filesystem", &Registration::unregisterFilesystem)
85
+ .define_method("to_data", [](const Registration &self) { return string_from_bytes(self.toData()); })
86
+ .define_method("path", [](const Registration &self) { return path_to_string(self.path()); })
87
+ .define_method("owner", &Registration::owner)
88
+ .define_method("version", &Registration::version)
89
+ .define_method("error", &Registration::error)
90
+ .define_method("ok?", &Registration::isOk);
91
+
92
+ Data_Type<PlaceHolderAttributes> rb_cPlaceHolderAttributes = Rice::define_class_under<PlaceHolderAttributes>(rb_mOpenVFS, "PlaceHolderAttributes")
93
+ .define_singleton_function("create", [](const std::string &path, const std::string &etag, const std::string &file_id, std::size_t size) {
94
+ return PlaceHolderAttributes::create(path_from_string(path), etag, file_id, size);
95
+ })
96
+ .define_singleton_function("from_data", [](const std::string &path, const std::string &data) {
97
+ return PlaceHolderAttributes::fromData(path_from_string(path), bytes_from_string(data));
98
+ })
99
+ .define_singleton_function("from_attributes", [](const std::string &path) {
100
+ return PlaceHolderAttributes::fromAttributes(path_from_string(path));
101
+ })
102
+ .define_method("to_data", [](const PlaceHolderAttributes &self) { return string_from_bytes(self.toData()); })
103
+ .define_method("real_size", &PlaceHolderAttributes::realSize)
104
+ .define_method("validate?", &PlaceHolderAttributes::validate)
105
+ .define_method("ok?", &PlaceHolderAttributes::isOk)
106
+ .define_method("absolute_path", [](const PlaceHolderAttributes &self) { return path_to_string(self.absolutePath); })
107
+ .define_method("absolute_path=", [](PlaceHolderAttributes &self, const std::string &path) { self.absolutePath = path_from_string(path); })
108
+ .define_attr("etag", &PlaceHolderAttributes::etag)
109
+ .define_attr("file_id", &PlaceHolderAttributes::fileId)
110
+ .define_attr("size", &PlaceHolderAttributes::size)
111
+ .define_attr("state", &PlaceHolderAttributes::state)
112
+ .define_attr("pin_state", &PlaceHolderAttributes::pinState);
113
+ }
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Deliberately not "../openvfs": that pulls in the openvfs extension, which wraps the placeholder attributes libopenvfs keeps in extended attributes. Nothing here needs them, and upstream ships no Windows build of that library.
4
+ require_relative "version"
5
+ require "openvfs/occ/occ"
6
+
7
+ require "fileutils"
8
+ require "securerandom"
9
+
10
+ module OpenVFS
11
+ # Writes the INI file AccountManager::restore and FolderMan::loadFolders read on start.
12
+ #
13
+ # The credential is not written here: credentials live in the keychain, so store it with Account#credential_manager.set("http/oauthtoken", credential) before starting.
14
+ #
15
+ # @parameter path [String] The local directory to sync.
16
+ # @parameter url [String] The server's base URL.
17
+ # @parameter remote_path [String] The WebDAV path to sync against.
18
+ # @parameter uuid [String] The account's UUID; also names the keychain entry.
19
+ # @parameter display_name [String] How the folder is named in logs.
20
+ # @parameter space_id [String] The space to sync, if any.
21
+ # @returns [String] The UUID the account was written under.
22
+ def self.configure(path, url:, remote_path:, uuid: SecureRandom.uuid, display_name: path, space_id: "")
23
+ config_path = File.join(Dir.home, ".config", "openvfs-rb", "openvfs-rb.cfg")
24
+ FileUtils.mkdir_p(File.dirname(config_path))
25
+ FileUtils.mkdir_p(path)
26
+
27
+ File.write(config_path, <<~CONFIG)
28
+ [Accounts]
29
+ 1\\url=#{url}
30
+ 1\\uuid=#{uuid}
31
+ size=1
32
+
33
+ [Folders]
34
+ 1\\accountUUID=#{uuid}
35
+ 1\\localPath=#{File.join(path, "")}
36
+ 1\\journalPath=.sync_journal.db
37
+ 1\\davUrl=#{remote_path}
38
+ 1\\spaceId=#{space_id}
39
+ 1\\displayString=#{display_name}
40
+ 1\\paused=false
41
+ 1\\ignoreHiddenFiles=true
42
+ 1\\deployed=false
43
+ 1\\priority=0
44
+ 1\\virtualFilesMode=off
45
+ size=1
46
+ CONFIG
47
+
48
+ uuid
49
+ end
50
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Nathan Kidd.
5
+
6
+ module OpenVFS
7
+ module Rb
8
+ VERSION = "1.0.0"
9
+ end
10
+ end
data/lib/openvfs/rb.rb ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Nathan Kidd.
5
+
6
+ require_relative "rb/version"
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Nathan Kidd.
5
+
6
+ require_relative "rb/version"
7
+
8
+ module OpenVFS
9
+ VERSION = Rb::VERSION
10
+ end
data/lib/openvfs-rb.rb ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Nathan Kidd.
5
+
6
+ require_relative "openvfs"
data/lib/openvfs.rb ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Nathan Kidd.
5
+
6
+ require_relative "openvfs/version"
7
+ require "openvfs/openvfs"
8
+
9
+ # A virtual filesystem.
10
+ module OpenVFS
11
+ end
data/license.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright, 2026, by Nathan Kidd.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
metadata ADDED
@@ -0,0 +1,82 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: openvfs-rb
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Nathan Kidd
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-01 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: gem_kit
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.2'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.2'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rice
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '4.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '4.0'
40
+ email:
41
+ - nathankidd@hey.com
42
+ executables: []
43
+ extensions:
44
+ - ext/openvfs/extconf.rb
45
+ - ext/openvfs/occ/extconf.rb
46
+ extra_rdoc_files: []
47
+ files:
48
+ - README.md
49
+ - ext/openvfs/extconf.rb
50
+ - ext/openvfs/occ/extconf.rb
51
+ - ext/openvfs/occ/sync.cpp
52
+ - ext/openvfs/ruby_openvfs.cpp
53
+ - lib/openvfs-rb.rb
54
+ - lib/openvfs.rb
55
+ - lib/openvfs/occ.rb
56
+ - lib/openvfs/rb.rb
57
+ - lib/openvfs/rb/version.rb
58
+ - lib/openvfs/version.rb
59
+ - license.md
60
+ homepage: https://github.com/n-at-han-k/openvfs-rb
61
+ licenses:
62
+ - MIT
63
+ metadata:
64
+ source_code_uri: https://github.com/n-at-han-k/openvfs-rb
65
+ rdoc_options: []
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '3.3'
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ requirements: []
79
+ rubygems_version: 3.7.2
80
+ specification_version: 4
81
+ summary: A virtual filesystem for Ruby.
82
+ test_files: []