1
2#' @theme assets/extra.css assets/rd.js
3#' @useDynLib zip, .registration = TRUE, .fixes = "c_"
4NULL
5
6#' Compress Files into 'zip' Archives
7#'
8#' `zip()` creates a new zip archive file.
9#'
10#' `zip_append()` appends compressed files to an existing 'zip' file.
11#'
12#' ## Relative paths
13#'
14#' `zip()` and `zip_append()` can run in two different modes: mirror
15#' mode and cherry picking mode. They handle the specified `files`
16#' differently.
17#'
18#' ### Mirror mode
19#'
20#' Mirror mode is for creating the zip archive of a directory structure,
21#' exactly as it is on the disk. The current working directory will
22#' be the root of the archive, and the paths will be fully kept.
23#' zip changes the current directory to `root` before creating the
24#' archive.
25#'
26#' (Absolute paths are also kept. Note that this might result
27#' non-portable archives: some zip tools do not handle zip archives that
28#' contain absolute file names, or file names that start with `../` or
29#' `./`. zip warns you if this should happen.)
30#'
31#' E.g. consider the following directory structure:
32#'
33#' ```{r echo = FALSE, comment = ""}
34#' dir.create(tmp <- tempfile())
35#' oldwd <- getwd()
36#' setwd(tmp)
37#' dir.create("foo/bar", recursive = TRUE)
38#' dir.create("foo/bar2")
39#' dir.create("foo2")
40#' cat("this is file1", file = "foo/bar/file1")
41#' cat("this is file2", file = "foo/bar/file2")
42#' cat("this is file3", file = "foo2/file3")
43#' out <- processx::run("tree", c("--noreport", "--charset=ascii"))
44#' cat(crayon::strip_style(out$stdout))
45#' setwd(oldwd)
46#' ```
47#'
48#' Assuming the current working directory is `foo`, the following zip
49#' entries are created by `zip`:
50#' ```{r, echo = 2:4}
51#' setwd(tmp)
52#' setwd("foo")
53#' zip::zip("../test.zip", c("bar/file1", "bar2", "../foo2"))
54#' zip_list("../test.zip")[, "filename", drop = FALSE]
55#' setwd(oldwd)
56#' ```
57#'
58#' ### Cherry picking mode
59#'
60#' In cherry picking mode, the selected files and directories
61#' will be at the root of the archive. This mode is handy if you
62#' want to select a subset of files and directories, possibly from
63#' different paths and put all of the in the archive, at the top
64#' level.
65#'
66#' Here is an example with the same directory structure as above:
67#'
68#' ```{r, echo = 3:4}
69#' setwd(tmp)
70#' setwd("foo")
71#' zip::zip(
72#'   "../test2.zip",
73#'   c("bar/file1", "bar2", "../foo2"),
74#'   mode = "cherry-pick"
75#')
76#' zip_list("../test2.zip")[, "filename", drop = FALSE]
77#' setwd(oldwd)
78#' ```
79#'
80#' ## Permissions:
81#'
82#' `zip()` (and `zip_append()`, etc.) add the permissions of
83#' the archived files and directories to the ZIP archive, on Unix systems.
84#' Most zip and unzip implementations support these, so they will be
85#' recovered after extracting the archive.
86#'
87#' Note, however that the owner and group (uid and gid) are currently
88#' omitted, even on Unix.
89#'
90#' ## `zipr()` and `zipr_append()`
91#'
92#' These function exist for historical reasons. They are identical
93#' to `zip()` and `zipr_append()` with a different default for the
94#' `mode` argument.
95#'
96#' @param zipfile The zip file to create. If the file exists, `zip`
97#'   overwrites it, but `zip_append` appends to it.
98#' @param files List of file to add to the archive. See details below
99#'    about absolute and relative path names.
100#' @param recurse Whether to add the contents of directories recursively.
101#' @param compression_level A number between 1 and 9. 9 compresses best,
102#'   but it also takes the longest.
103#' @param include_directories Whether to explicitly include directories
104#'   in the archive. Including directories might confuse MS Office when
105#'   reading docx files, so set this to `FALSE` for creating them.
106#' @param root Change to this working directory before creating the
107#'   archive.
108#' @param mode Selects how files and directories are stored in
109#'   the archice. It can be `"mirror"` or `"cherry-pick"`.
110#'   See "Relative Paths" below for details.
111#' @return The name of the created zip file, invisibly.
112#'
113#' @export
114#' @examples
115#' ## Some files to zip up. We will run all this in the R sesion's
116#' ## temporary directory, to avoid messing up the user's workspace.
117#' dir.create(tmp <- tempfile())
118#' dir.create(file.path(tmp, "mydir"))
119#' cat("first file", file = file.path(tmp, "mydir", "file1"))
120#' cat("second file", file = file.path(tmp, "mydir", "file2"))
121#'
122#' zipfile <- tempfile(fileext = ".zip")
123#' zip::zip(zipfile, "mydir", root = tmp)
124#'
125#' ## List contents
126#' zip_list(zipfile)
127#'
128#' ## Add another file
129#' cat("third file", file = file.path(tmp, "mydir", "file3"))
130#' zip_append(zipfile, file.path("mydir", "file3"), root = tmp)
131#' zip_list(zipfile)
132
133zip <- function(zipfile, files, recurse = TRUE, compression_level = 9,
134                include_directories = TRUE, root = ".",
135                mode = c("mirror", "cherry-pick")) {
136  mode <- match.arg(mode)
137  zip_internal(zipfile, files, recurse, compression_level, append = FALSE,
138               root = root, keep_path = (mode == "mirror"),
139               include_directories = include_directories)
140}
141
142#' @rdname zip
143#' @export
144
145zipr <- function(zipfile, files, recurse = TRUE, compression_level = 9,
146                 include_directories = TRUE, root = ".",
147                 mode = c("cherry-pick", "mirror")) {
148  mode <- match.arg(mode)
149  zip_internal(zipfile, files, recurse, compression_level, append = FALSE,
150               root = root, keep_path = (mode == "mirror"),
151               include_directories = include_directories)
152}
153
154#' @rdname zip
155#' @export
156
157zip_append <- function(zipfile, files, recurse = TRUE,
158                       compression_level = 9, include_directories = TRUE,
159                       root = ".", mode = c("mirror", "cherry-pick")) {
160  mode <- match.arg(mode)
161  zip_internal(zipfile, files, recurse, compression_level, append = TRUE,
162               root = root, keep_path = (mode == "mirror"),
163               include_directories = include_directories)
164}
165
166#' @rdname zip
167#' @export
168
169zipr_append <- function(zipfile, files, recurse = TRUE,
170                        compression_level = 9, include_directories = TRUE,
171                        root = ".", mode = c("cherry-pick", "mirror")) {
172  mode <- match.arg(mode)
173  zip_internal(zipfile, files, recurse, compression_level, append = TRUE,
174               root = root, keep_path = (mode == "mirror"),
175               include_directories = include_directories)
176}
177
178zip_internal <- function(zipfile, files, recurse, compression_level,
179                         append, root, keep_path, include_directories) {
180  oldwd <- setwd(root)
181  on.exit(setwd(oldwd), add = TRUE)
182
183  if (any(! file.exists(files))) stop("Some files do not exist")
184
185  data <- get_zip_data(files, recurse, keep_path, include_directories)
186  warn_for_dotdot(data$key)
187
188  .Call(c_R_zip_zip, enc2utf8(zipfile), enc2utf8(data$key),
189        enc2utf8(data$file), data$dir, file.info(data$file)$mtime,
190        as.integer(compression_level), append)
191
192  invisible(zipfile)
193}
194
195#' List Files in a 'zip' Archive
196#'
197#' @details Note that `crc32` is formatted using `as.hexmode()`. `offset` refers
198#'   to the start of the local zip header for each entry. Following the approach
199#'   of `seek()` it is stored as a `numeric` rather than an `integer` vector and
200#'   can therefore represent values up to `2^53-1` (9 PB).
201#' @param zipfile Path to an existing ZIP file.
202#' @return A data frame with columns: `filename`, `compressed_size`,
203#'   `uncompressed_size`, `timestamp`, `permissions`, `crc32` and `offset`.
204#'
205#' @family zip/unzip functions
206#' @export
207
208zip_list <- function(zipfile) {
209  zipfile <- enc2utf8(normalizePath(zipfile))
210  res <- .Call(c_R_zip_list, zipfile)
211  df <- data.frame(
212    stringsAsFactors = FALSE,
213    filename = res[[1]],
214    compressed_size = res[[2]],
215    uncompressed_size = res[[3]],
216    timestamp = as.POSIXct(res[[4]], tz = "UTC", origin = "1970-01-01")
217  )
218  Encoding(df$filename) <- "UTF-8"
219  df$permissions <- as.octmode(res[[5]])
220  df$crc32 <- as.hexmode(res[[6]])
221  df$offset <- res[[7]]
222  df
223}
224
225#' Uncompress 'zip' Archives
226#'
227#' `unzip()` always restores modification times of the extracted files and
228#' directories.
229#'
230#' @section Permissions:
231#'
232#' If the zip archive stores permissions and was created on Unix,
233#' the permissions will be restored.
234#'
235#' @param zipfile Path to the zip file to uncompress.
236#' @param files Character vector of files to extract from the archive.
237#'   Files within directories can be specified, but they must use a forward
238#'   slash as path separator, as this is what zip files use internally.
239#'   If `NULL`, all files will be extracted.
240#' @param overwrite Whether to overwrite existing files. If `FALSE` and
241#'   a file already exists, then an error is thrown.
242#' @param junkpaths Whether to ignore all directory paths when creating
243#'   files. If `TRUE`, all files will be created in `exdir`.
244#' @param exdir Directory to uncompress the archive to. If it does not
245#'   exist, it will be created.
246#'
247#' @export
248#' @examples
249#' ## temporary directory, to avoid messing up the user's workspace.
250#' dir.create(tmp <- tempfile())
251#' dir.create(file.path(tmp, "mydir"))
252#' cat("first file", file = file.path(tmp, "mydir", "file1"))
253#' cat("second file", file = file.path(tmp, "mydir", "file2"))
254#'
255#' zipfile <- tempfile(fileext = ".zip")
256#' zip::zip(zipfile, "mydir", root = tmp)
257#'
258#' ## List contents
259#' zip_list(zipfile)
260#'
261#' ## Extract
262#' tmp2 <- tempfile()
263#' unzip(zipfile, exdir = tmp2)
264#' dir(tmp2, recursive = TRUE)
265
266unzip <- function(zipfile, files = NULL, overwrite = TRUE,
267                      junkpaths = FALSE, exdir = ".") {
268
269  stopifnot(
270    is_string(zipfile),
271    is_character_or_null(files),
272    is_flag(overwrite),
273    is_flag(junkpaths),
274    is_string(exdir))
275
276  zipfile <- enc2utf8(normalizePath(zipfile))
277  if (!is.null(files)) files <- enc2utf8(files)
278  mkdirp(exdir)
279  exdir <- enc2utf8(normalizePath(exdir))
280
281  .Call(c_R_zip_unzip, zipfile, files, overwrite, junkpaths, exdir)
282
283  invisible()
284}
285