Module zip

Le module zip permet de manipuler des archives Zip depuis un script YScript : ouvrir une archive existante ou en créer une nouvelle, lister son contenu, ajouter/remplacer/supprimer des entrées, et compresser/décompresser un dossier entier.

require("io")
local zip = require("zip")

local ar = zip.open("package.zip", "a")
for _, e in ipairs(ar:entries()) do
    print(e.name, e.size, e.isdirectory)
end
ar:add("readme.txt", "hello")
ar:close()

Prérequis : la bibliothèque io

zip n'a aucune fonctionnalité qui ne touche pas au système de fichiers : une archive est un fichier, ajouter une entrée depuis un dossier lit des fichiers, compresser un dossier les parcourt. La bibliothèque io est donc une dépendance obligatoire de tout le module (contrairement à des modules comme http, où io n'est nécessaire qu'à certaines fonctions).

io doit être chargée avant zip :

require("io")          -- ou l'hôte a déjà appelé OpenIOLib()
local zip = require("zip")

Si io n'est pas chargée, require("zip") échoue (l'erreur remonte comme n'importe quel loader qui échoue — utilisez pcall(require, "zip") si vous voulez récupérer nil, message plutôt que de laisser l'erreur se propager) :

local ok, err = pcall(require, "zip")
if not ok then
    print("zip indisponible : " .. tostring(err))
end

Vue d'ensemble

Fonction / méthode Description
zip.open(path [, mode]) Ouvre ou crée une archive, retourne l'objet archive
zip.type(obj) "archive" / "closed archive" / nil
zip.extractall(zippath, destdir [, overwrite]) Décompresse une archive entière vers un dossier
zip.compressdir(srcdir, destzippath [, options]) Compresse un dossier entier vers une nouvelle archive
archive:entries() Liste le contenu de l'archive
archive:exists(name) Teste la présence d'une entrée
archive:read(name) Lit le contenu d'une entrée en mémoire
archive:add(name, content) Ajoute une nouvelle entrée depuis une chaîne
archive:addfile(name, filepath) Ajoute une nouvelle entrée depuis un fichier du disque
archive:replace(name, content) Remplace le contenu d'une entrée existante
archive:remove(name) Supprime une entrée
archive:extract(name, destpath [, overwrite]) Extrait une entrée vers un fichier du disque
archive:extractall(destdir [, overwrite]) Extrait toutes les entrées vers un dossier
archive:close() Referme l'archive

L'objet archive

zip.open retourne un objet archive (un userdata avec ses propres méthodes, dans le même esprit qu'un FILE* retourné par io.open). Il doit être refermé explicitement par archive:close(), implicitement via l'attribut <close>, ou en dernier recours par le ramasse-miettes :

local ar <close> = zip.open("package.zip", "r")
-- ar:close() est appelé automatiquement en sortie de bloc

Utiliser une méthode sur une archive déjà fermée lève une erreur.


zip.open(path [, mode])

Ouvre une archive existante ou en crée une nouvelle. mode (chaîne, défaut "r") :

Mode Comportement
"r" Lecture seule. Erreur (nil, message) si le fichier n'existe pas ou n'est pas un zip valide.
"w" Nouvelle archive vide. Écrase silencieusement un fichier existant au même chemin.
"a" Ouvre l'archive existante en modification, ou en crée une vide si elle n'existe pas encore. Permet ajout/remplacement/suppression.

En cas d'échec (fichier absent en "r", contenu invalide, erreur disque...), zip.open retourne nil suivi d'un message d'erreur — comme io.open — plutôt que de lever une erreur script :

local ar, err = zip.open("introuvable.zip", "r")
if not ar then
    print("échec : " .. err)
    return
end

Un mode autre que "r"/"w"/"a" est en revanche une erreur de programmation et lève une erreur script immédiate (mauvais argument), pas un nil, message.

zip.type(obj)

Miroir de io.type : retourne "archive" si obj est une archive ouverte, "closed archive" si elle est fermée, ou nil si obj n'est pas une archive de ce module.

local ar = zip.open("a.zip", "w")
print(zip.type(ar))    --> archive
ar:close()
print(zip.type(ar))    --> closed archive
print(zip.type("x"))   --> nil

archive:entries()

Retourne un tableau (indices 1..n) décrivant chaque entrée de l'archive :

Champ Type Description
name string Chemin de l'entrée dans l'archive, séparateur /
size integer Taille décompressée en octets
compressedsize integer Taille compressée en octets
lastmodified integer Horodatage (epoch, secondes) — même unité que os.time()
isdirectory boolean true si l'entrée représente un dossier (nom finissant par /)
local ar = zip.open("package.zip", "r")
for _, e in ipairs(ar:entries()) do
    print(string.format("%-30s %8d -> %8d octets", e.name, e.size, e.compressedsize))
end
ar:close()

archive:exists(name)

Retourne true/false selon la présence d'une entrée name (fichier ou dossier) dans l'archive. Utile avant :add/:replace/:remove, qui sont volontairement stricts (voir plus bas) :

if ar:exists("config.json") then
    ar:replace("config.json", newContent)
else
    ar:add("config.json", newContent)
end

archive:read(name)

Retourne le contenu entier de l'entrée name sous forme de chaîne binaire (un caractère = un octet, même convention que le mode binaire de io) — pratique pour les petits fichiers texte/JSON, mais charge tout le contenu en mémoire de script. Pour un gros fichier, préférez :extract qui passe par un flux sans tout charger.

Erreurs : entrée absente, ou entrée désignant un dossier.

local ar = zip.open("package.zip", "r")
local manifest = ar:read("package.json")
ar:close()

archive:add(name, content) / archive:addfile(name, filepath)

Créent une nouvelle entrée name. Une entrée déjà présente sous ce nom est une erreur (pas d'écrasement implicite — utilisez :replace, ou testez :exists au préalable si vous voulez un comportement « upsert »).

local ar = zip.open("package.zip", "a")
ar:add("VERSION", "1.0.0\n")
ar:addfile("dist/app.dll", "bin/Release/app.dll")
ar:close()

archive:replace(name, content)

Remplace le contenu d'une entrée déjà présente. Le format Zip ne permettant pas de modifier une entrée en place, ceci équivaut en interne à une suppression suivie d'un ajout — transparent pour le script, mais à garder en tête si vous inspectez le flux de l'archive par un autre moyen pendant l'opération. Erreur si name est absente (symétrique de :add).

ar:replace("VERSION", "1.0.1\n")

archive:remove(name)

Supprime l'entrée name. Erreur si elle est absente.

ar:remove("old-file.txt")

archive:extract(name, destpath [, overwrite])

Extrait l'entrée name directement vers le fichier destpath du disque, en flux (sans charger le contenu en mémoire de script — pendant symétrique de :addfile).

overwrite (booléen, défaut false) : si destpath existe déjà et overwrite n'est pas true, c'est une erreur — pas d'écrasement silencieux d'un fichier existant.

Erreur si l'entrée est absente, ou si elle désigne un dossier (utilisez :extractall ou io.mkdir pour un dossier).

local ar = zip.open("package.zip", "r")
ar:extract("bin/app.dll", "install/app.dll", true)  -- true = écrase si déjà présent
ar:close()

archive:extractall(destdir [, overwrite])

Extrait toutes les entrées vers destdir, en recréant la structure de dossiers automatiquement (sous-dossiers créés au besoin). overwrite a la même sémantique que pour :extract, appliquée à chaque fichier extrait.

local ar = zip.open("package.zip", "r")
ar:extractall("installed/mypackage", true)
ar:close()

archive:close()

Referme l'archive : persiste les modifications en attente (pour une archive ouverte en "w"/"a") et referme le fichier sous-jacent. Idempotent — un second :close() ne fait rien.


zip.extractall(zippath, destdir [, overwrite])

Raccourci équivalent à zip.open(zippath, "r"):extractall(destdir, overwrite) suivi d'un :close() automatique. Pratique pour un cas d'usage en un seul appel, par exemple installer un paquet téléchargé :

zip.extractall("mypackage-1.0.0.zip", "installed/mypackage", true)

zip.compressdir(srcdir, destzippath [, options])

Crée une nouvelle archive à destzippath et y ajoute récursivement tout le contenu de srcdir, avec des chemins d'entrée relatifs à srcdir (séparateur /, y compris depuis un hôte Windows). Une erreur est levée si srcdir n'existe pas.

options (table optionnelle) :

Option Défaut Description
overwrite false Si destzippath existe déjà et overwrite n'est pas true, c'est une erreur (contrairement à zip.open(path, "w"), qui écrase toujours — ici l'écrasement en masse doit être explicite).
includebase false Si true, les entrées sont préfixées par le nom du dossier srcdir lui-même (srcdir/fichier.txt) plutôt que par son seul contenu (fichier.txt).
-- Sans includebase : les entrées sont "fichier.txt", "sub/nested.txt", ...
zip.compressdir("dist/mypackage", "mypackage-1.0.0.zip", { overwrite = true })

-- Avec includebase : les entrées sont "mypackage/fichier.txt", "mypackage/sub/nested.txt", ...
zip.compressdir("dist/mypackage", "mypackage-1.0.0.zip", { includebase = true })

Erreurs

Les erreurs de programmation (mauvais type/valeur d'argument, mode invalide, méthode appelée sur une archive fermée, opération incohérente comme ajouter une entrée déjà présente) sont levées comme des erreurs script classiques — à capturer avec pcall si nécessaire. Seul zip.open fait exception pour les échecs liés au système de fichiers (archive absente en lecture, contenu invalide, erreur disque) : il retourne nil, message, comme io.open.

Situation Comportement
require("zip") sans io chargée require échoue (erreur du loader)
zip.open(path, "r") sur un fichier absent nil, message
zip.open(path, "r") sur un fichier qui n'est pas un zip valide nil, message
zip.open(path, mode) avec un mode autre que "r"/"w"/"a" erreur script (mauvais argument)
Méthode appelée sur une archive déjà fermée erreur script
:add/:addfile sur un nom déjà présent erreur script
:read/:replace/:remove/:extract sur un nom absent erreur script
:read/:extract visant une entrée qui est un dossier erreur script
:addfile avec un fichier disque absent erreur script
:extract/:extractall vers un chemin existant sans overwrite erreur script
zip.compressdir avec un srcdir absent erreur script
zip.compressdir sans overwrite sur une destination déjà existante erreur script

Exemple complet

require("io")
local zip = require("zip")

-- Empaqueter un dossier de sources en une archive
zip.compressdir("dist/mypackage", "mypackage-1.0.0.zip", { overwrite = true })

-- Inspecter le contenu d'une archive
local ar = zip.open("mypackage-1.0.0.zip", "r")
for _, e in ipairs(ar:entries()) do
    print(e.name, e.size, e.isdirectory)
end
local manifest = ar:read("package.json")
ar:close()

-- Construire/mettre à jour une archive entrée par entrée
local pkg = zip.open("mypackage-1.0.0.zip", "a")
pkg:replace("package.json", manifest .. "\n-- updated")
pkg:addfile("CHANGELOG.md", "CHANGELOG.md")
pkg:remove("old-file.txt")
pkg:close()

-- "Installer" un paquet : tout extraire dans un dossier
zip.extractall("mypackage-1.0.0.zip", "installed/mypackage", true)

Limites connues (v1)