init
[garradin.git] / include / class.sauvegarde.php
1 <?php
2
3 namespace Garradin;
4
5 class Sauvegarde
6 {
7 const NEED_UPGRADE = 'nu';
8
9 /**
10 * Renvoie la liste des fichiers SQLite sauvegardés
11 * @param boolean $auto Si true ne renvoie que la liste des sauvegardes automatiques
12 * @return array Liste des fichiers
13 */
14 public function getList($auto = false)
15 {
16 $ext = $auto ? 'auto\.\d+\.sqlite' : 'sqlite';
17
18 $out = [];
19 $dir = dir(DATA_ROOT);
20
21 while ($file = $dir->read())
22 {
23 if ($file[0] != '.' && is_file(DATA_ROOT . '/' . $file)
24 && preg_match('![\w\d._-]+\.' . $ext . '$!i', $file) && $file != basename(DB_FILE))
25 {
26 $out[$file] = filemtime(DATA_ROOT . '/' . $file);
27 }
28 }
29
30 $dir->close();
31
32 ksort($out);
33
34 return $out;
35 }
36
37 /**
38 * Crée une nouvelle sauvegarde
39 * @param boolean $auto Si true le nom de fichier sera celui de la sauvegarde automatique courante,
40 * sinon le nom sera basé sur la date (sauvegarde manuelle)
41 * @return string Le nom de fichier de la sauvegarde ainsi créée
42 */
43 public function create($auto = false)
44 {
45 $backup = str_replace('.sqlite', ($auto ? '.auto.1' : date('.Y-m-d-His')) . '.sqlite', DB_FILE);
46 copy(DB_FILE, $backup);
47 return basename($backup);
48 }
49
50 /**
51 * Effectue une rotation des sauvegardes automatiques
52 * association.auto.1.sqlite deviendra association.auto.2.sqlite par exemple
53 * @return boolean true
54 */
55 public function rotate()
56 {
57 $config = Config::getInstance();
58 $nb = $config->get('nombre_sauvegardes');
59
60 $list = $this->getList(true);
61 krsort($list);
62
63 if (count($list) >= $nb)
64 {
65 $this->remove(key($list));
66 array_shift($list);
67 }
68
69 foreach ($list as $f=>$d)
70 {
71 $new = preg_replace_callback('!\.auto\.(\d+)\.sqlite$!', function ($m) {
72 return '.auto.' . ((int) $m[1] + 1) . '.sqlite';
73 }, $f);
74
75 rename(DATA_ROOT . '/' . $f, DATA_ROOT . '/' . $new);
76 }
77
78 return true;
79 }
80
81 /**
82 * Crée une sauvegarde automatique si besoin est
83 * @return boolean true
84 */
85 public function auto()
86 {
87 $config = Config::getInstance();
88
89 // Pas besoin d'aller plus loin si on ne fait pas de sauvegarde auto
90 if ($config->get('frequence_sauvegardes') == 0 || $config->get('nombre_sauvegardes') == 0)
91 return true;
92
93 $list = $this->getList(true);
94
95 if (count($list) > 0)
96 {
97 $last = current($list);
98 }
99 else
100 {
101 $last = false;
102 }
103
104 // Test de la date de création de la dernière sauvegarde
105 if ($last >= (time() - ($config->get('frequence_sauvegardes') * 3600 * 24)))
106 {
107 return true;
108 }
109
110 // Si pas de modif depuis la dernière sauvegarde, ça sert à rien d'en faire
111 if ($last >= filemtime(DB_FILE))
112 {
113 return true;
114 }
115
116 $this->rotate();
117 $this->create(true);
118
119 return true;
120 }
121
122 /**
123 * Efface une sauvegarde locale
124 * @param string $file Nom du fichier à supprimer
125 * @return boolean true si le fichier a bien été supprimé, false sinon
126 */
127 public function remove($file)
128 {
129 if (preg_match('!\.\.+!', $file) || !preg_match('!^[\w\d._-]+\.sqlite$!i', $file)
130 || $file == basename(DB_FILE))
131 {
132 throw new UserException('Nom de fichier non valide.');
133 }
134
135 return unlink(DATA_ROOT . '/' . $file);
136 }
137
138 /**
139 * Renvoie sur la sortie courante le contenu du fichier de base de données courant
140 * @return boolean true
141 */
142 public function dump()
143 {
144 $in = fopen(DB_FILE, 'r');
145 $out = fopen('php://output', 'w');
146
147 while (!feof($in))
148 {
149 fwrite($out, fread($in, 8192));
150 }
151
152 fclose($in);
153 fclose($out);
154 return true;
155 }
156
157 /**
158 * Restaure une sauvegarde locale
159 * @param string $file Le nom de fichier à utiliser comme point de restauration
160 * @return boolean true si la restauration a fonctionné, false sinon
161 */
162 public function restoreFromLocal($file)
163 {
164 if (preg_match('!\.\.+!', $file) || !preg_match('!^[\w\d._-]+$!i', $file))
165 {
166 throw new UserException('Nom de fichier non valide.');
167 }
168
169 if (!file_exists(DATA_ROOT . '/' . $file))
170 {
171 throw new UserException('Le fichier fourni n\'existe pas.');
172 }
173
174 return $this->restoreDB(DATA_ROOT . '/' . $file);
175 }
176
177 /**
178 * Restaure une copie distante (fichier envoyé)
179 * @param array $file Tableau provenant de $_FILES
180 * @return boolean true
181 */
182 public function restoreFromUpload($file)
183 {
184 if (empty($file['size']) || empty($file['tmp_name']) || !empty($file['error']))
185 {
186 throw new UserException('Le fichier n\'a pas été correctement envoyé. Essayer de le renvoyer à nouveau.');
187 }
188
189 $r = $this->restoreDB($file['tmp_name']);
190
191 if ($r)
192 {
193 unlink($file['tmp_name']);
194 }
195
196 return $r;
197 }
198
199 /**
200 * Restauration de base de données, la fonction qui le fait vraiment
201 * @param string $file Chemin absolu vers la base de données à utiliser
202 * @return mixed true si rien ne va plus, ou self::NEED_UPGRADE si la version de la DB
203 * ne correspond pas à la version de Garradin (mise à jour nécessaire).
204 */
205 protected function restoreDB($file)
206 {
207 // Essayons déjà d'ouvrir la base de données à restaurer en lecture
208 try {
209 $db = new \SQLite3($file, SQLITE3_OPEN_READONLY);
210 }
211 catch (\Exception $e)
212 {
213 throw new UserException('Le fichier fourni n\'est pas une base de données valide. ' .
214 'Message d\'erreur de SQLite : ' . $e->getMessage());
215 }
216
217 // Regardons ensuite si la base de données n'est pas corrompue
218 $check = $db->querySingle('PRAGMA integrity_check;');
219
220 if (strtolower(trim($check)) != 'ok')
221 {
222 throw new UserException('Le fichier fourni est corrompu. SQLite a trouvé ' . $check . ' erreurs.');
223 }
224
225 // On ne peut pas faire de vérifications très poussées sur la structure de la base de données,
226 // celle-ci pouvant changer d'une version à l'autre et on peut vouloir importer une base
227 // un peu vieille, mais on vérifie quand même que ça ressemble un minimum à une base garradin
228 $table = $db->querySingle('SELECT 1 FROM sqlite_master WHERE type=\'table\' AND tbl_name=\'config\';');
229
230 if (!$table)
231 {
232 throw new UserException('Le fichier fourni ne semble pas contenir de données liées à Garradin.');
233 }
234
235 // On récupère la version pour plus tard
236 $version = $db->querySingle('SELECT valeur FROM config WHERE cle=\'version\';');
237
238 $db->close();
239
240 $backup = str_replace('.sqlite', date('.Y-m-d-His') . '.avant_restauration.sqlite', DB_FILE);
241
242 if (!rename(DB_FILE, $backup))
243 {
244 throw new \RuntimeException('Unable to backup current DB file.');
245 }
246
247 if (!copy($file, DB_FILE))
248 {
249 rename($backup, DB_FILE);
250 throw new \RuntimeException('Unable to copy backup DB to main location.');
251 }
252
253 if ($version != garradin_version())
254 {
255 return self::NEED_UPGRADE;
256 }
257
258 return true;
259 }
260
261 }
262
263 ?>