-
Notifications
You must be signed in to change notification settings - Fork 0
/
ReSync.groovy
343 lines (292 loc) · 11 KB
/
ReSync.groovy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
/**
* ReSync will perform the following actions on a reMarkable2 Tablet:
*
* 1. Backup templates and images
* 2. Copy custom templates and images to reMarkable2
* 3. Update the templates Json file with custom templates and removed templates
* 4. Reboot the reMarkable2
*/
class ReSync {
static final String CUSTOM_TEMPLATES_DIR = './templates/'
static final String CUSTOM_IMAGES_DIR = './images/'
static final String WORK_DIR_PARENT = './work/'
static final String ORIG_FILE_EXTENSION = '.orig'
static final String TEMPLATES_JSON_FILENAME = 'templates.json'
static final String TEMPLATES_TO_EXCLUDE_FILENAME = 'excludes.txt'
static final String RM_HOME_DIR = './'
static final String RM_ROOT_DIR = '/usr/share/remarkable/'
static final String RM_TEMPLATE_DIR = RM_ROOT_DIR + 'templates/'
static final String RM_NOTEBOOK_DIR = RM_HOME_DIR + '.local/share/remarkable/xochitl/'
static final String RM_TEMPLATES_JSON_FILENAME = RM_TEMPLATE_DIR + TEMPLATES_JSON_FILENAME
SshConnection sshConn
String timestamp
String workDir
static void main(String[] args) {
def numArgs = args.size()
def validArgs = ['sync', 'test']
if (numArgs != 1) {
println "Error: Incorrect number of options. Must use a single option."
ReSync.usage()
} else if (validArgs.contains(args[0])) {
ReSync reSync = new ReSync()
reSync.connect()
if (args[0] == 'sync') {
println "Performing full synchronization of templates and images."
reSync.performSync()
} else if (args[0] == 'test') {
println "Testing connection."
} else if (args[0] == 'backup') {
println "Performing backup of notebook data."
reSync.backupNotebookFiles()
}
reSync.disconnect()
} else {
println "Error: Invalid option '${args[0]}'."
ReSync.usage()
}
}
/**
* Constructor to initialize class variables
*/
ReSync() {
sshConn = new SshConnection()
timestamp = createSessionTimestamp()
makeWorkDirectory()
}
/**
* Display usage information.
*/
static void usage() {
println "./run [option]"
println " options:"
println " sync - performs full synchronization of templates and images"
println " test - verify connectivity with ReMarkable"
println " backup - perform backup of notebook files"
}
/**
* Connects to the reMarkable tablet through an SSH session.
*/
void connect() {
if (sshConn.connect()) {
println 'Connected.'
} else {
println 'Error connecting to reMarkable2. Exiting'
System.exit(1)
}
}
/**
* Disconnects a reMarkable SSH session.
*/
void disconnect() {
sshConn.disconnect()
}
void performSync() {
backupReMarkableFiles()
updateTemplates()
copyFilesToReMarkable()
rebootReMarkable()
println 'reMarkable reSync complete!'
}
/**
* Creates a directory in the local filesystem to store files used during the session.
*/
void makeWorkDirectory() {
workDir = WORK_DIR_PARENT + timestamp + '/'
new File(workDir).mkdirs()
}
/**
* Facilitates the creation and transfer of reMarkable backups.
*/
void backupReMarkableFiles() {
// Create backups on reMarkable2
String templatesBackupFile = createBackupTarGz('templates', RM_TEMPLATE_DIR)
String imagesBackupFile = createBackupTarGz('images', RM_ROOT_DIR + '*.png')
// Transfer backups to local
sshConn.scpRemoteToLocal(templatesBackupFile, workDir)
sshConn.scpRemoteToLocal(imagesBackupFile, workDir)
}
/**
* Facilitates the transfer of notebook backups.
*
* This operation takes a considerable amount of time due to up to 8 GB of
* notebook data.
*/
void backupNotebookFiles() {
String notebookBackupFile = createBackupTarGz('notebooks', RM_NOTEBOOK_DIR)
sshConn.scpRemoteToLocal(notebookBackupFile, workDir)
}
/**
* Copies the contents of a local directory to a reMarkable directory, using SCP.
*
* @param String localDirectory path of local directory to copy from
* @param String remarkableDirectory path of remarkable directory to copy to
*/
void copyDirectoryContentsToRemarkable(String localDirectory, String remarkableDirectory) {
File directory = new File(localDirectory)
directory.eachFile { file ->
println 'Transferring ' + file
sshConn.scpLocalToRemote(file.toString(), remarkableDirectory)
}
}
void copyFilesToReMarkable() {
copyImagesToReMarkable()
copyTemplatesToReMarkable()
copyJsonToReMarkable()
}
void copyImagesToReMarkable() {
copyDirectoryContentsToRemarkable(CUSTOM_IMAGES_DIR, RM_ROOT_DIR)
}
void copyTemplatesToReMarkable() {
copyDirectoryContentsToRemarkable(CUSTOM_TEMPLATES_DIR, RM_TEMPLATE_DIR)
}
void copyJsonToReMarkable() {
String newJsonTemplatesFile = workDir + TEMPLATES_JSON_FILENAME
sshConn.scpLocalToRemote(newJsonTemplatesFile, RM_TEMPLATE_DIR)
}
void updateTemplates() {
fetchTemplatesJsonFile()
Map origJsonData = extractJsonFromFile(new File(workDir + TEMPLATES_JSON_FILENAME + ORIG_FILE_EXTENSION))
Map jsonData = removeUnusedTemplatesFromJson(origJsonData)
// Convert templates to lists
List templates = []
jsonData.templates.each { template ->
templates << template
}
// Add custom templates
Map newJsonData = extractJsonFromFile(new File('custom_' + TEMPLATES_JSON_FILENAME))
newJsonData.templates.each { newJson ->
templates << newJson
}
convertTemplatesListToJsonFile(templates)
}
void convertTemplatesListToJsonFile(List templates) {
Map jsonData = ['templates':templates]
String jsonOutput = JsonOutput.toJson(jsonData)
String prettyJsonOutput = JsonOutput.prettyPrint(jsonOutput)
File newJsonTemplateFile = new File(workDir + TEMPLATES_JSON_FILENAME)
newJsonTemplateFile.write(prettyJsonOutput)
}
Map extractJsonFromFile(File jsonFile) {
JsonSlurper jsonSlurper = new JsonSlurper()
Map jsonData = jsonSlurper.parse(jsonFile)
return jsonData
}
Map removeUnusedTemplatesFromJson(Map origJsonData) {
List templatesToExclude = getListOfTemplatesToExclude()
origJsonData.templates.removeAll { originalTemplate ->
originalTemplate.filename in templatesToExclude
}
return origJsonData
}
List getListOfTemplatesToExclude() {
List templatesToExclude = []
new File(TEMPLATES_TO_EXCLUDE_FILENAME).eachLine { templateFilename ->
templatesToExclude << templateFilename
}
return templatesToExclude
}
void rebootReMarkable() {
println 'Restarting reMarkable xochitl service...'
sshConn.runCommand('systemctl restart xochitl')
}
/**
* Obtains the templates.json file from the reMarkable2
*/
void fetchTemplatesJsonFile() {
String localFilename = workDir + TEMPLATES_JSON_FILENAME + ORIG_FILE_EXTENSION
sshConn.scpRemoteToLocal(RM_TEMPLATES_JSON_FILENAME, localFilename)
}
/**
* Creates gzipped tarball of a target directory or files
*
* @param archive String base filename of gzipped tarball to create
* @param target String path of files to backup
*
* @return String filename of gzipped tarball created
*/
String createBackupTarGz(String archive, String target) {
String fullArchiveFilename = getArchiveFilename(archive)
sshConn.runCommand(getTarGzCommand(fullArchiveFilename, target))
waitForStableFileSize(fullArchiveFilename)
return fullArchiveFilename
}
/**
* Constructs a full filename for an archive based on the archive name and timestamp.
*
* @param basename the base of the archive filename to create, without timestamp or extension
* @return the full filename of archive file to create, with timestamp and extension
*/
String getArchiveFilename(String basename) {
return "${basename}_${timestamp}.tar.gz"
}
/**
* Constructs a tar gz command to create the archive based on the archive name and target.
*
* @param archiveFilename filename of archive to create
* @param target filepath of archive contents
* @return command to run to produce the archive
*/
String getTarGzCommand(String archiveFilename, String target) {
return "tar -zcvf ${archiveFilename} ${target}"
}
/**
* Waits until a filesize is stable (no longer growing).
*
* @param filepath full path and filename of remote file to monitor
*/
void waitForStableFileSize(String filePath) {
int sleepTimeBetweenFilesizeChecksInMillis = 250
int fileSize = getRemoteFileSize(filePath)
int lastFileSize = -1
println "Waiting for stable filesize for ${filePath}"
def countOfSameReadings = 0
while (fileSize != lastFileSize || countOfSameReadings < 3) {
sleep(sleepTimeBetweenFilesizeChecksInMillis)
lastFileSize = fileSize
fileSize = getRemoteFileSize(filePath)
// Make sure there are 3 readings of the same size in a row
if (fileSize == lastFileSize) {
countOfSameReadings++
} else {
countOfSameReadings = 0
}
}
}
/**
* Fetches the filesize of a remote file.
*
* @param filePath full path and filename of the file to size check
* @return filesize in bytes
*/
int getRemoteFileSize(String filePath) {
Map results = sshConn.runCommandGetOutput(getLsCommand(filePath))
long fileSize = 0
if (results.exitStatus != 0) {
println 'Error: Command failed.'
} else {
// Example output: [drwxr-xr-x, 3, root, root, 12288, Aug, 2, 09:06, templates.bak]
fileSize = results.output.split(' +')[4] as Integer
println 'Size of ' + filePath + ' = ' + fileSize
}
return fileSize
}
/**
* Constructs an `ls` command to get file attributes, including the size in bytes.
*
* @param filename full path and filename of file to use in construction of command
* @return command to run to produce the `ls` results
*/
String getLsCommand(String filename) {
return "ls -l ${filename}"
}
/**
* Obtains current date/time.
*
* @return string representation of current date/time
*/
String createSessionTimestamp() {
return new Date().format('yyMMdd-HHmm')
}
}