-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathrotate.R
351 lines (317 loc) · 10.6 KB
/
rotate.R
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
344
345
346
347
348
349
350
351
#' Rotate or backup files
#'
#' @description
#' Functions starting with `backup` create backups of a `file`, while functions
#' starting with `rotate` do the same but also replace the original `file`
#' with an empty one (this is useful for log rotation)
#'
#' **Note:**: `rotate()` and co will not work reliable on filenames that contain
#' dots but have no file extension (e.g. `my.holiday.picture.jpg` is OK but
#' `my.holiday.picture` is not)
#'
#'
#' @param file `character` scalar: file to backup/rotate
#'
#' @param age minimum age after which to backup/rotate a file; can be
#' - a `character` scalar representing an Interval in the form
#' `"<number> <interval>"` (e.g. `"2 months"`, see *Intervals* section below).
#' - a `Date` or a `character` scalar representing a Date for
#' a fixed point in time after which to backup/rotate. See `format` for
#' which Date/Datetime formats are supported by rotor.
#'
#' (if `age` *and* `size` are provided, both criteria must be `TRUE` to
#' trigger rotation)
#' @param format a scalar `character` that can be a subset of of valid
#' `strftime()` formatting strings. The default setting is
#' `"%Y-%m-%d--%H-%M-%S"`.
#' * You can use an arbitrary number of dashes anywhere in the format, so
#' `"%Y-%m-%d--%H-%M-%S"` and `"%Y%m%d%H%M%S"` are both legal.
#' * `T` and `_` can also be used as separators. For example, the following
#' datetime formats are also possible:
#' `%Y-%m-%d_%H-%M-%S` (Python logging default),
#' `%Y%m%dT%H%M%S` ([ISO 8601](https://en.wikipedia.org/wiki/ISO_8601))
#' * All datetime components except `%Y` are optional. If you leave out part
#' of the timestamp, the first point in time in the period is assumed. For
#' example (assuming the current year is 2019) `%Y` is identical to
#' `2019-01-01--00-00-00`.
#' * The timestamps must be lexically sortable, so `"%Y-%m-%d"` is legal,
#' `"%m-%d-%Y"` and `%Y-%d` are not.
#'
#' @param now The current `Date` or time (`POSIXct`) as a scalar. You can pass a
#' custom value here to to override the real system time. As a convenience you
#' can also pass in `character` strings that follow the guidelines outlined
#' above for `format`, but please note that these differ from the formats
#' understood by [as.POSIXct()] or [as.Date()].
#'
#' @param max_backups maximum number of backups to keep
#' - an `integer` scalar: Maximum number of backups to keep
#'
#' In addition for timestamped backups the following value are supported:
#' - a `Date` scalar: Remove all backups before this date
#' - a `character` scalar representing a Date in ISO format (e.g. `"2019-12-31"`)
#' - a `character` scalar representing an Interval in the form `"<number> <interval>"` (see below for more info)
#'
#' @param size scalar `integer`, `character` or `Inf`. Backup/rotate only if
#' `file` is larger than this size. `Integers` are interpreted as bytes. You
#' can pass `character` vectors that contain a file size suffix like `1k`
#' (kilobytes), `3M` (megabytes), `4G` (gigabytes), `5T` (terabytes). Instead
#' of these short forms you can also be explicit and use the IEC suffixes
#' `KiB`, `MiB`, `GiB`, `TiB`. In Both cases `1` kilobyte is `1024` bytes, 1
#' `megabyte` is `1024` kilobytes, etc... .
#'
#' (if `age` *and* `size` are provided, both criteria must be `TRUE` to
#' trigger rotation)
#'
#' @param dir `character` scalar. The directory in which the backups
#' of `file` are stored (defaults to `dirname(file)`)
#'
#' @param compression Whether or not backups should be compressed
#' - `FALSE` for uncompressed backups,
#' - `TRUE` for zip compression; uses [zip()]
#' - a scalar `integer` between `1` and `9` to specify a compression
#' level (requires the
#' [zip](https://CRAN.R-project.org/package=zip) package,
#' see its documentation for details)
#' - the `character` scalars `"utils::zip()"` or `"zip::zipr"` to force a
#' specific zip command
#'
#' @param dry_run `logical` scalar. If `TRUE` no changes are applied to the
#' file system (no files are created or deleted)
#'
#' @param verbose `logical` scalar. If `TRUE` additional informative `messages`
#' are printed
#'
#' @param create_file `logical` scalar. If `TRUE` create an empty file in
#' place of `file` after rotating.
#'
#' @param overwrite `logical` scalar. If `TRUE` overwrite backups if a backup
#' of the same name (usually due to timestamp collision) exists.
#'
#' @return `file` as a `character` scalar (invisibly)
#'
#' @section Side Effects:
#' `backup()`, `backup_date()`, and `backup_time()` may create files (if the
#' specified conditions are met). They may also delete backups, based on
#' `max_backup`.
#'
#' `rotate()`, `rotate_date()` and `rotate_time()` do the same, but in
#' addition delete the input `file`, or replace it with an empty file if
#' `create_file == TRUE` (the default).
#'
#' @section Intervals:
#'
#' In **rotor**, an interval is a character string in the form
#' `"<number> <interval>"`. The following intervals are possible:
#' `"day(s)"`, `"week(s)"`, `"month(s)"`, `"quarter(s)"`, `"year(s)"`.
#' The plural `"s"` is optional (so `"2 weeks"` and `"2 week"` are equivalent).
#' Please be aware that weeks are
#' [ISOweeks](https://en.wikipedia.org/wiki/ISO_week_date)
#' and start on Monday (not Sunday as in some countries).
#'
#' Interval strings can be used as arguments when backing up or rotating files,
#' or for pruning backup queues (i.e. limiting the number of backups of a
#' single) file.
#'
#' When rotating/backing up `"1 months"` means "make a new backup if the last
#' backup is from the preceding month". E.g if the last backup of `myfile`
#' is from `2019-02-01` then `backup_time(myfile, age = "1 month")` will only
#' create a backup if the current date is at least `2019-03-01`.
#'
#' When pruning/limiting backup queues, `"1 year"` means "keep at least most
#' one year worth of backups". So if you call
#' `backup_time(myfile, max_backups = "1 year")` on `2019-03-01`, it will create
#' a backup and then remove all backups of `myfile` before `2019-01-01`.
#' @seealso [list_backups()]
#' @export
#'
#' @examples
#' # setup example file
#' tf <- tempfile("test", fileext = ".rds")
#' saveRDS(cars, tf)
#'
#' # create two backups of `tf``
#' backup(tf)
#' backup(tf)
#' list_backups(tf) # find all backups of a file
#'
#' # If `size` is set, a backup is only created if the target file is at least
#' # that big. This is more useful for log rotation than for backups.
#' backup(tf, size = "100 mb") # no backup becuase `tf` is to small
#' list_backups(tf)
#'
#' # If `dry_run` is TRUE, backup() only shows what would happen without
#' # actually creating or deleting files
#' backup(tf, size = "0.1kb", dry_run = TRUE)
#'
#' # rotate() is the same as backup(), but replaces `tf`` with an empty file
#' rotate(tf)
#' list_backups(tf)
#' file.size(tf)
#' file.size(list_backups(tf))
#'
#' # prune_backups() can remove old backups
#' prune_backups(tf, 1) # keep only one backup
#' list_backups(tf)
#'
#' # rotate/backup_date() adds a date instead of an index
#' # you should not mix index backups and timestamp backups
#' # so we clean up first
#' prune_backups(tf, 0)
#' saveRDS(cars, tf)
#'
#' # backup_date() adds the date instead of an index to the filename
#' backup_date(tf)
#'
#' # `age` sets the minimum age of the last backup before creating a new one.
#' # the example below creates no new backup since it's less than a week
#' # since the last.
#' backup_date(tf, age = "1 week")
#'
#' # `now` overrides the current date.
#' backup_date(tf, age = "1 year", now = "2999-12-31")
#' list_backups(tf)
#'
#' # backup_time() creates backups with a full timestamp
#' backup_time(tf)
#'
#' # It's okay to mix backup_date() and backup_time()
#' list_backups(tf)
#'
#' # cleanup
#' prune_backups(tf, 0)
#' file.remove(tf)
rotate <- function(
file,
size = 1,
max_backups = Inf,
compression = FALSE,
dir = dirname(file),
create_file = TRUE,
dry_run = FALSE,
verbose = dry_run
){
rotate_internal(
file,
size = size,
max_backups = max_backups,
compression = compression,
dir = dir,
create_file = create_file,
dry_run = dry_run,
verbose = verbose,
do_rotate = TRUE
)
}
#' @rdname rotate
#' @export
backup <- function(
file,
size = 0,
max_backups = Inf,
compression = FALSE,
dir = dirname(file),
dry_run = FALSE,
verbose = dry_run
){
rotate_internal(
file,
size = size,
max_backups = max_backups,
compression = compression,
dir = dir,
dry_run = dry_run,
verbose = verbose,
create_file = FALSE,
do_rotate = FALSE
)
}
rotate_internal <- function(
file,
size,
max_backups,
compression,
create_file,
dir,
dry_run,
verbose,
do_rotate
){
stopifnot(
is_scalar_bool(do_rotate),
is_scalar_bool(dry_run),
is_scalar_bool(verbose),
is_scalar_bool(create_file)
)
assert_pure_BackupQueue(file, dir = dir, warn_only = TRUE)
if (dry_run){
DRY_RUN$activate()
on.exit(DRY_RUN$deactivate())
}
bq <- BackupQueueIndex$new(
file,
dir = dir,
max_backups = max_backups,
compression = compression
)
if (bq$should_rotate(size = size, verbose = verbose)){
bq$push()
} else {
do_rotate <- FALSE
}
bq$prune(max_backups)
if (do_rotate){
file_remove(file)
if (create_file)
file_create(file)
}
invisible(file)
}
#' @description `prune_backups()` physically deletes all backups of a file
#' based on `max_backups`
#' @section Side Effects:
#' `prune_backups()` may delete files, depending on `max_backups`.
#' @export
#' @rdname rotate
prune_backups <- function(
file,
max_backups,
dir = dirname(file),
dry_run = FALSE,
verbose = dry_run
){
assert_pure_BackupQueue(file, dir = dir)
assert(is_scalar_character(file))
if (dry_run){
DRY_RUN$activate()
on.exit(DRY_RUN$deactivate())
}
bq <- BackupQueueIndex$new(file, dir = dir)
if (!bq$has_backups)
bq <- BackupQueueDateTime$new(file, dir = dir)
bq$prune(max_backups = max_backups)
invisible(file)
}
#' @description `prune_backups()` physically deletes all backups of a file
#' based on `max_backups`
#' @section Side Effects:
#' `prune_backups()` may delete files, depending on `max_backups`.
#' @export
#' @rdname rotate
prune_identical_backups <- function(
file,
dir = dirname(file),
dry_run = FALSE,
verbose = dry_run
){
assert_pure_BackupQueue(file, dir = dir)
assert(is_scalar_character(file))
if (dry_run){
DRY_RUN$activate()
on.exit(DRY_RUN$deactivate())
}
bq <- BackupQueueIndex$new(file, dir = dir)
if (!bq$has_backups)
bq <- BackupQueueDateTime$new(file, dir = dir)
bq$prune_identical()
invisible(file)
}