Skip to content
API Documentation
BastionGuard™
SmbMount.vala
Go to the documentation of this file.
1/*
2 * SmbMount.vala
3 *
4 * Copyright 2025-2026 Calogero Scarnà — BastionGuard Backup
5 *
6 * GPL-2.0-or-later
7 *
8 * Helper class to mount, list and unmount SMB/CIFS network shares
9 * (NAS, Samba servers, Windows shared folders) so that snapshots
10 * can be created/restored/listed from these locations.
11 */
12
13using GLib;
14using Gee;
15using Json;
16
17using TeeJee.Logging;
21using TeeJee.Misc;
22
23/**
24 * Represents a single SMB/CIFS share that BastionGuard Backup is
25 * aware of. Either currently-mounted or remembered for re-use.
26 */
27public class SmbShare : GLib.Object {
28
29 public string server = ""; // e.g. "nas.local" or "192.168.1.10"
30 public string share = ""; // e.g. "backups"
31 public string subpath = ""; // optional path inside the share, e.g. "machineA"
32 public string username = "";
33 public string domain = "";
34 public string label = ""; // free-text user label (optional)
35
36 public string mount_point = ""; // populated once mounted
37
38 // password is intentionally NOT serialized to disk
39 public string password = "";
40 public bool save_credentials = false;
41
42 public string unc_path {
43 owned get {
44 string s = "//%s/%s".printf(server, share);
45 if (subpath.length > 0){
46 s += "/" + subpath;
47 }
48 return s;
49 }
50 }
51
52 public string display_name {
53 owned get {
54 if (label.length > 0){
55 return "%s (%s)".printf(label, unc_path);
56 }
57 return unc_path;
58 }
59 }
60
61 public Json.Object to_json(){
62 var obj = new Json.Object();
63 obj.set_string_member("server", server);
64 obj.set_string_member("share", share);
65 obj.set_string_member("subpath", subpath);
66 obj.set_string_member("username", username);
67 obj.set_string_member("domain", domain);
68 obj.set_string_member("label", label);
69 return obj;
70 }
71
72 public static SmbShare from_json(Json.Object obj){
73 var s = new SmbShare();
74 s.server = json_get_string(obj, "server", "");
75 s.share = json_get_string(obj, "share", "");
76 s.subpath = json_get_string(obj, "subpath", "");
77 s.username = json_get_string(obj, "username", "");
78 s.domain = json_get_string(obj, "domain", "");
79 s.label = json_get_string(obj, "label", "");
80 return s;
81 }
82}
83
84/**
85 * Static helper: drives mount.cifs, tracks active mounts and
86 * persists the user's recently-used shares.
87 */
88public class SmbMount : GLib.Object {
89
90 // Where we mount remote shares. Lives under the per-pid app
91 // mount_point dir (set up by Main) so cleanup follows the
92 // existing pattern.
93 public static string mount_root {
94 owned get {
95 return (App != null && App.mount_point_app.length > 0)
96 ? App.mount_point_app + "/smb"
97 : "/run/bastionguard-backup/smb";
98 }
99 }
100
101 // JSON file where we remember the user's network shares
102 // (everything except the password).
103 private const string SHARES_CONF = "/etc/bastionguard-backup/smb-shares.json";
104
105 /**
106 * Returns true if both `mount.cifs` and `cifs-utils` look usable
107 * on this system. Sets `error_message` with a hint otherwise.
108 */
109 public static bool is_available(out string error_message){
110 error_message = "";
111 if (!cmd_exists("mount.cifs")){
112 error_message = _("Network shares (SMB/CIFS) are not available on this system.")
113 + "\n\n"
114 + _("Please install the 'cifs-utils' package and try again.");
115 return false;
116 }
117 return true;
118 }
119
120 /**
121 * Mount the SMB share described by `share`. On success the
122 * `mount_point` field of `share` is populated and the path is
123 * returned. On failure returns an empty string and `error_message`
124 * carries a translated explanation.
125 */
126 public static string mount(SmbShare share, out string error_message){
127 error_message = "";
128
129 if (share.server.length == 0 || share.share.length == 0){
130 error_message = _("Server and share name must both be provided.");
131 return "";
132 }
133
134 string avail_err;
135 if (!is_available(out avail_err)){
136 error_message = avail_err;
137 return "";
138 }
139
140 // Build a stable-but-readable mount point.
141 string slug = "%s_%s".printf(
142 share.server.replace("/", "_").replace(":", "_"),
143 share.share.replace("/", "_"));
144 string mp = path_combine(mount_root, slug);
145 dir_create(mp);
146
147 // Already mounted at our path? Reuse.
148 if (is_mounted_at(mp)){
149 share.mount_point = mp;
150 log_msg(_("SMB share already mounted at") + ": %s".printf(mp));
151 return mp;
152 }
153
154 // Build mount options. We deliberately keep them conservative
155 // so most NASes "just work" while still allowing rsync to set
156 // permissions and create hardlinks.
157 var opts = new Gee.ArrayList<string>();
158
159 // Credentials. Empty user => guest mount.
160 if (share.username.length > 0){
161 opts.add("username=" + share.username);
162 if (share.password.length > 0){
163 opts.add("password=" + share.password);
164 }
165 if (share.domain.length > 0){
166 opts.add("domain=" + share.domain);
167 }
168 }
169 else{
170 opts.add("guest");
171 }
172
173 // Run as root inside the mount (rsync needs this to
174 // preserve ownership of system files).
175 opts.add("uid=0");
176 opts.add("gid=0");
177 opts.add("file_mode=0644");
178 opts.add("dir_mode=0755");
179
180 // Prefer the most modern dialect that is widely supported.
181 opts.add("vers=3.0");
182
183 // rsync uses unix sockets/temp files inside the share, so allow
184 // "noperm" as a fallback if the server is overly strict. This
185 // is safe because the device side already gates access.
186 opts.add("noperm");
187
188 string opt_str = string.joinv(",", opts.to_array());
189
190 string unc = "//%s/%s".printf(share.server, share.share);
191 // mount.cifs takes the share without the optional subpath; the
192 // subpath is just appended to the resulting mount_point later.
193
194 string cmd = "mount -t cifs -o %s \"%s\" \"%s\"".printf(
195 Shell.quote(opt_str).replace("'", "'\\''"),
196 unc,
197 mp);
198
199 // Avoid leaking the password into the log
200 string safe_opt_str = opt_str;
201 if (share.password.length > 0){
202 safe_opt_str = safe_opt_str.replace(
203 "password=" + share.password, "password=***");
204 }
205 log_debug("SmbMount.mount(): mount -t cifs -o %s %s %s".printf(
206 safe_opt_str, unc, mp));
207
208 string std_out, std_err;
209 int ret_val = exec_sync(cmd, out std_out, out std_err);
210
211 if (ret_val != 0){
212 // Map a few common errors to friendlier messages.
213 string err = (std_err ?? "").strip();
214 if (err.length == 0){ err = (std_out ?? "").strip(); }
215
216 if (err.down().contains("permission denied")
217 || err.down().contains("logon failure")
218 || err.down().contains("nt_status_logon_failure")){
219 error_message = _("Authentication failed. Please check the username, password and domain.");
220 }
221 else if (err.down().contains("no route to host")
222 || err.down().contains("connection refused")
223 || err.down().contains("connection timed out")
224 || err.down().contains("host is down")){
225 error_message = _("Could not reach the server. Please check the address and that the share is online.");
226 }
227 else if (err.down().contains("no such file or directory")
228 || err.down().contains("bad share name")){
229 error_message = _("The share was not found on the server. Please check the share name.");
230 }
231 else{
232 error_message = _("Failed to mount the network share.") + "\n\n" + err;
233 }
234 log_error(error_message);
235 return "";
236 }
237
238 share.mount_point = mp;
239 log_msg(_("Mounted network share") + ": %s → %s".printf(unc, mp));
240 return mp;
241 }
242
243 public static bool unmount(SmbShare share){
244 if (share.mount_point.length == 0){
245 return true;
246 }
247 bool ok = Device.unmount(share.mount_point);
248 if (ok){
249 share.mount_point = "";
250 }
251 return ok;
252 }
253
254 public static bool unmount_path(string mount_point){
255 if (mount_point.length == 0){
256 return true;
257 }
258 return Device.unmount(mount_point);
259 }
260
261 public static bool is_mounted_at(string mount_point){
262 try {
263 string contents;
264 FileUtils.get_contents("/proc/mounts", out contents);
265 foreach (string line in contents.split("\n")){
266 var parts = line.split(" ");
267 if (parts.length >= 3 && parts[1] == mount_point && parts[2] == "cifs"){
268 return true;
269 }
270 }
271 }
272 catch (Error e){
273 log_debug(e.message);
274 }
275 return false;
276 }
277
278 /**
279 * Returns every CIFS mount currently visible in /proc/mounts as
280 * an SmbShare with the `mount_point` field populated. Username
281 * and credentials are NOT recoverable from the kernel.
282 */
283 public static Gee.ArrayList<SmbShare> list_active_mounts(){
284 var list = new Gee.ArrayList<SmbShare>();
285 try {
286 string contents;
287 FileUtils.get_contents("/proc/mounts", out contents);
288 foreach (string line in contents.split("\n")){
289 if (line.strip().length == 0){ continue; }
290 var parts = line.split(" ");
291 if (parts.length < 3){ continue; }
292 if (parts[2] != "cifs" && parts[2] != "smb3"){ continue; }
293
294 string source = parts[0]; // e.g. //server/share
295 string mp = parts[1];
296
297 // /proc/mounts escapes spaces as \040 etc — decode.
298 mp = mp.replace("\\040", " ");
299
300 var share = new SmbShare();
301 share.mount_point = mp;
302 if (source.has_prefix("//")){
303 string rest = source.substring(2);
304 int slash = rest.index_of("/");
305 if (slash > 0){
306 share.server = rest.substring(0, slash);
307 share.share = rest.substring(slash + 1);
308 }
309 else {
310 share.server = rest;
311 }
312 }
313 list.add(share);
314 }
315 }
316 catch (Error e){
317 log_debug(e.message);
318 }
319 return list;
320 }
321
322 // ---- persistence of remembered shares ------------------------
323
324 public static Gee.ArrayList<SmbShare> load_saved_shares(){
325 var list = new Gee.ArrayList<SmbShare>();
326 var f = File.new_for_path(SHARES_CONF);
327 if (!f.query_exists()){
328 return list;
329 }
330 try {
331 var parser = new Json.Parser();
332 parser.load_from_file(SHARES_CONF);
333 var root = parser.get_root();
334 if (root == null){ return list; }
335 var arr = root.get_array();
336 if (arr == null){ return list; }
337 arr.foreach_element((a, idx, node) => {
338 var obj = node.get_object();
339 if (obj != null){
340 list.add(SmbShare.from_json(obj));
341 }
342 });
343 }
344 catch (Error e){
345 log_error("SmbMount.load_saved_shares(): " + e.message);
346 }
347 return list;
348 }
349
350 public static void save_share(SmbShare share){
351 // merge: replace if the same server+share+subpath already exists,
352 // otherwise append.
353 var list = load_saved_shares();
354 bool replaced = false;
355 for (int i = 0; i < list.size; i++){
356 var existing = list[i];
357 if (existing.server == share.server
358 && existing.share == share.share
359 && existing.subpath == share.subpath){
360 list[i] = share;
361 replaced = true;
362 break;
363 }
364 }
365 if (!replaced){
366 list.add(share);
367 }
368 write_shares(list);
369 }
370
371 public static void forget_share(SmbShare share){
372 var list = load_saved_shares();
373 var keep = new Gee.ArrayList<SmbShare>();
374 foreach (var existing in list){
375 if (existing.server == share.server
376 && existing.share == share.share
377 && existing.subpath == share.subpath){
378 continue;
379 }
380 keep.add(existing);
381 }
382 write_shares(keep);
383 }
384
385 private static void write_shares(Gee.ArrayList<SmbShare> list){
386 try {
387 dir_create(GLib.Path.get_dirname(SHARES_CONF));
388 var arr = new Json.Array();
389 foreach (var s in list){
390 arr.add_object_element(s.to_json());
391 }
392 var node = new Json.Node(Json.NodeType.ARRAY);
393 node.set_array(arr);
394
395 var generator = new Json.Generator();
396 generator.pretty = true;
397 generator.set_root(node);
398 generator.to_file(SHARES_CONF);
399
400 // Best effort: lock down the file because it contains
401 // usernames (no passwords, but still).
402 Posix.chmod(SHARES_CONF, 0600);
403 }
404 catch (Error e){
405 log_error("SmbMount.write_shares(): " + e.message);
406 }
407 }
408}
Main App
#define _(s)
static bool unmount(string mount_point)
Definition Device.vala:1723
string mount_point_app
Definition Main.vala:82
static bool unmount(SmbShare share)
static bool is_mounted_at(string mount_point)
static void forget_share(SmbShare share)
static string mount(SmbShare share, out string error_message)
const string SHARES_CONF
static bool is_available(out string error_message)
static void write_shares(Gee.ArrayList< SmbShare > list)
static string mount_root
Definition SmbMount.vala:93
static void save_share(SmbShare share)
static bool unmount_path(string mount_point)
static Gee.ArrayList< SmbShare > load_saved_shares()
static Gee.ArrayList< SmbShare > list_active_mounts()
string subpath
Definition SmbMount.vala:31
string display_name
Definition SmbMount.vala:52
static SmbShare from_json(Json.Object obj)
Definition SmbMount.vala:72
string password
Definition SmbMount.vala:39
string mount_point
Definition SmbMount.vala:36
bool save_credentials
Definition SmbMount.vala:40
Json.Object to_json()
Definition SmbMount.vala:61
string domain
Definition SmbMount.vala:33
string label
Definition SmbMount.vala:34
string username
Definition SmbMount.vala:32
string server
Definition SmbMount.vala:29
string share
Definition SmbMount.vala:30
string unc_path
Definition SmbMount.vala:42
string path_combine(string path1, string path2)
bool dir_create(string dir_path, bool show_message=false)
string json_get_string(Json.Object jobj, string member, string def_value)
void log_debug(string message)
void log_error(string message, bool highlight=false, bool is_warning=false)
void log_msg(string message, bool highlight=false)
int exec_sync(string cmd, out string? std_out=null, out string? std_err=null)