1 /**
2 Copyright: Copyright (c) 2019, Joakim Brännström. All rights reserved.
3 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0)
4 Author: Joakim Brännström (joakim.brannstrom@gmx.com)
5 */
6 module dsnapshot.backend;
7 
8 import logger = std.experimental.logger;
9 import std.algorithm : map, filter;
10 import std.datetime : SysTime;
11 
12 import dsnapshot.backend.crypt;
13 import dsnapshot.backend.rsync;
14 import dsnapshot.config;
15 import dsnapshot.exception;
16 import dsnapshot.from;
17 import dsnapshot.process;
18 public import dsnapshot.layout : Layout;
19 public import dsnapshot.types;
20 
21 @safe:
22 
23 /**
24  * Error handling is via exceptions.
25  */
26 interface SyncBackend {
27     /// Execute a command on the host that is the destination of the snapshots.
28     void remoteCmd(const RemoteHost host, const RemoteSubCmd cmd, const string path);
29 
30     /// Update layout of the snapshots at the destination.
31     Layout update(Layout layout);
32 
33     /// Publish the snapshot in dst.
34     void publishSnapshot(const string newSnapshot);
35 
36     /// Remove discarded snapshots.
37     void removeDiscarded(const Layout layout);
38 
39     /// Sync from src to dst.
40     void sync(const Layout layout, const SnapshotConfig snapshot, const string nameOfNewSnapshot);
41 
42     /// Restore dst to src.
43     void restore(const Layout layout, const SnapshotConfig snapshot,
44             const SysTime time, const string restoreTo);
45 
46     /// The flow of data that the backend handles.
47     Flow flow();
48 }
49 
50 SyncBackend makeSyncBackend(SnapshotConfig s) {
51     auto rval = s.syncCmd.match!((None a) { return null; },
52             (RsyncConfig a) => new RsyncBackend(a, s.remoteCmd, null));
53 
54     if (rval is null) {
55         logger.infof("No backend specified for %s. Supported are: rsync", s.name);
56         throw new Exception(null);
57     }
58     return rval;
59 }
60 
61 SyncBackend makeSyncBackend(SnapshotConfig s, const dsnapshot.config.Config.Backup backup) {
62     auto rval = s.syncCmd.match!((None a) { return null; },
63             (RsyncConfig a) => new RsyncBackend(a, s.remoteCmd, backup.ignoreRsyncErrorCodes));
64 
65     if (rval is null) {
66         logger.infof("No backend specified for %s. Supported are: rsync", s.name);
67         throw new Exception(null);
68     }
69     return rval;
70 }
71 
72 /**
73  */
74 class CryptException : Exception {
75     enum Kind {
76         generic,
77         wrongPassword,
78         errorWhenOpening,
79         errorWhenClosing,
80         noEncryptedSrc,
81     }
82 
83     Kind kind;
84 
85     this(string msg, Kind kind = Kind.generic, string file = __FILE__, int line = __LINE__) @safe pure nothrow {
86         super(msg, file, line);
87         this.kind = kind;
88     }
89 }
90 
91 /** Encryption of the snapshot destination.
92  *
93  * One backend only hold at most one target open at a time.
94  *
95  * Exceptions are used to signal errors.
96  *
97  * The API excepts this call sequence:
98  * One open followed by multiple close.
99  * or
100  * failed open followed by close.
101  */
102 interface CryptBackend {
103     /** Open the encrypted destination.
104      *
105      * Params:
106      * decrypted = where to make the decrypted data visible (mount it).
107      */
108     void open(const string decrypted);
109 
110     /// Close the encrypted destination.
111     void close();
112 
113     /// If the crypto backend supports hard links and thus --link-dest with e.g. rsync works.
114     bool supportHardLinks();
115 
116     /// If the crypto backend supports that the encrypted data is on a remote host.
117     bool supportRemoteEncryption();
118 }
119 
120 // TODO: root and mountPoint is probably not "generic" but until another crypt backend is added this will have to do.
121 CryptBackend makeCrypBackend(const CryptConfig c) {
122     return c.match!((const None a) => cast(CryptBackend) new PlainText,
123             (const EncFsConfig a) => new EncFs(a.configFile, a.passwd, a.encryptedPath,
124                 a.mountCmd, a.mountFuseOpts, a.unmountCmd, a.unmountFuseOpts));
125 }
126 
127 /** Open the destination that flow point to.
128  *
129  * Throws an exception if the destination is not local.
130  */
131 void open(CryptBackend be, const Flow flow) {
132     flow.match!((None a) {}, (FlowLocal a) { be.open(a.dst.value); }, (FlowRsyncToLocal a) {
133         be.open(a.dst.value);
134     }, (FlowLocalToRsync a) {
135         if (be.supportRemoteEncryption)
136             be.open(null); // TODO for now not implemented
137         else
138             throw new CryptException("Opening an encryption on a remote host is not supported",
139                 CryptException.Kind.errorWhenOpening);
140     });
141 }