Overview
Comment:Migrate more from Fichiers to File and Files
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | dev
Files: files | file ages | folders
SHA1: 332a8b64949a1f330a218c9617602e4386c7f505
User & Date: bohwaz on 2020-12-10 20:01:47
Other Links: branch diff | manifest | tags
Context
2020-12-11
01:11
Some kind of progress, or not check-in: cd88e798f2 user: bohwaz tags: dev
2020-12-10
20:01
Migrate more from Fichiers to File and Files check-in: 332a8b6494 user: bohwaz tags: dev
18:21
Merge with trunk check-in: ab8bff586d user: bohwaz tags: dev
Changes

Modified src/include/data/1.1.0_schema.sql from [d979d9fd8b] to [28445b9f61].

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
-- Files metadata
(
    id INTEGER NOT NULL PRIMARY KEY,
    path TEXT NOT NULL,
    name TEXT NOT NULL, -- file name (eg. image1234.jpeg)
    type TEXT NULL, -- MIME type
    image INTEGER NOT NULL DEFAULT 0, -- 1 = image reconnue






    created TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK (datetime(created) IS NOT NULL AND datetime(created) = created),
    modified TEXT NULL CHECK (datetime(modified) IS NULL OR datetime(modified) = modified),
    content_id INTEGER NOT NULL REFERENCES files_contents (id) ON DELETE CASCADE,
    author_id INTEGER NULL REFERENCES membres (id) ON DELETE SET NULL
);

CREATE INDEX IF NOT EXISTS files_path ON files (path);
CREATE INDEX IF NOT EXISTS files_date ON files (datetime);


CREATE TABLE IF NOT EXISTS files_contents
-- Contenu des fichiers
(
    id INTEGER NOT NULL PRIMARY KEY,
    hash TEXT NOT NULL, -- Hash SHA1 du contenu du fichier
    size INTEGER NOT NULL, -- Taille en octets
    content BLOB NULL,
    storage TEXT NULL, -- Storage medium, NULL means stored in content BLOB
    storage_path TEXT NULL
);

CREATE UNIQUE INDEX IF NOT EXISTS files_hash ON files_contents (hash);

CREATE TABLE IF NOT EXISTS files_folders
(
    id INTEGER NOT NULL PRIMARY KEY,
    parent_id INTEGER NULL REFERENCES files_folders(id) ON DELETE CASCADE,
    name TEXT NOT NULL
);

CREATE VIRTUAL TABLE IF NOT EXISTS files_search USING fts4
-- Search inside files content
(
    id INT PRIMARY KEY NOT NULL REFERENCES files_contents(id),
    content TEXT NULL -- Text content
);

CREATE TABLE IF NOT EXISTS files_links
-- This references use of a file outside of the documents module

(
    id INTEGER NOT NULL PRIMARY KEY,
    file_id INTEGER NOT NULL REFERENCES fichiers (id) ON DELETE CASCADE,
    user_id INTEGER NULL REFERENCES membres (id) ON DELETE CASCADE,
    transaction_id INTEGER NULL REFERENCES acc_transactions (id) ON DELETE CASCADE,
    config TEXT NULL REFERENCES config (valeur) ON DELETE CASCADE,
    web INTEGER NULL,
    -- Make sure that only one is filled
    CHECK ((user_id IS NULL) + (transaction_id IS NULL) + (config IS NULL) + (web IS NULL) = 1)
);

CREATE UNIQUE INDEX files_links_unique ON files_links (file_id, user_id, transaction_id, config, web);

-- FIXME: rename to english
CREATE TABLE IF NOT EXISTS recherches
-- Recherches enregistrées







>
>
>
>
>
>


|





>






<
|
<
<


|

















>

|
|





|







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
-- Files metadata
(
    id INTEGER NOT NULL PRIMARY KEY,
    path TEXT NOT NULL,
    name TEXT NOT NULL, -- file name (eg. image1234.jpeg)
    type TEXT NULL, -- MIME type
    image INTEGER NOT NULL DEFAULT 0, -- 1 = image reconnue
    size INTEGER NOT NULL DEFAULT 0,
    hash TEXT NOT NULL, -- Hash SHA1 du contenu du fichier

    storage TEXT NULL, -- Storage medium, NULL means stored in content BLOB
    storage_path TEXT NULL,

    created TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK (datetime(created) IS NOT NULL AND datetime(created) = created),
    modified TEXT NULL CHECK (datetime(modified) IS NULL OR datetime(modified) = modified),

    author_id INTEGER NULL REFERENCES membres (id) ON DELETE SET NULL
);

CREATE INDEX IF NOT EXISTS files_path ON files (path);
CREATE INDEX IF NOT EXISTS files_date ON files (datetime);
CREATE INDEX IF NOT EXISTS files_hash ON files (hash);

CREATE TABLE IF NOT EXISTS files_contents
-- Contenu des fichiers
(
    id INTEGER NOT NULL PRIMARY KEY,
    hash TEXT NOT NULL, -- Hash SHA1 du contenu du fichier

    content BLOB NULL


);

CREATE UNIQUE INDEX IF NOT EXISTS files_contents_hash ON files_contents (hash);

CREATE TABLE IF NOT EXISTS files_folders
(
    id INTEGER NOT NULL PRIMARY KEY,
    parent_id INTEGER NULL REFERENCES files_folders(id) ON DELETE CASCADE,
    name TEXT NOT NULL
);

CREATE VIRTUAL TABLE IF NOT EXISTS files_search USING fts4
-- Search inside files content
(
    id INT PRIMARY KEY NOT NULL REFERENCES files_contents(id),
    content TEXT NULL -- Text content
);

CREATE TABLE IF NOT EXISTS files_links
-- This references use of a file outside of the documents module
-- One file can only be linked to one thing
(
    id INTEGER NOT NULL PRIMARY KEY REFERENCES fichiers (id) ON DELETE CASCADE,
    file_id INTEGER NULL REFERENCES fichiers (id) ON DELETE CASCADE,
    user_id INTEGER NULL REFERENCES membres (id) ON DELETE CASCADE,
    transaction_id INTEGER NULL REFERENCES acc_transactions (id) ON DELETE CASCADE,
    config TEXT NULL REFERENCES config (valeur) ON DELETE CASCADE,
    web INTEGER NULL,
    -- Make sure that only one is filled
    CHECK ((user_id IS NULL) + (transaction_id IS NULL) + (config IS NULL) + (web IS NULL) + (file_id IS NULL) = 1)
);

CREATE UNIQUE INDEX files_links_unique ON files_links (file_id, user_id, transaction_id, config, web);

-- FIXME: rename to english
CREATE TABLE IF NOT EXISTS recherches
-- Recherches enregistrées

Modified src/include/lib/Garradin/Entities/Files/File.php from [4fd57e6360] to [dee3022a27].

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
use Garradin\UserException;

class File extends Entity
{
	const TABLE = 'files';

	protected $id;

	protected $name;













	protected $_types = [
		'id'         => 'int',






		'label'      => 'name',




	];





















	public function selfCheck(): void
	{
		parent::selfCheck();
	}

	public function delete(): bool
	{
		$return = parent::delete();

		// Clean up files
		Files::deleteOrphanFiles();




		return $return;
	}

	static protected function store(?string $path, string $name, string $source_path = null, $source_content = null): self
	{
		assert($path || $content);







>

>
>
>
>

>
>
>
>
>
>
>
>

|
>
>
>
>
>
>
|
>
>
>
>


>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>









|
|
>
>
>







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
use Garradin\UserException;

class File extends Entity
{
	const TABLE = 'files';

	protected $id;
	protected $path;
	protected $name;
	protected $type;
	protected $image;
	protected $size;
	protected $hash;

	protected $storage;
	protected $storage_path;

	protected $created;
	protected $modified;

	protected $author_id;

	protected $_types = [
		'id'           => 'int',
		'path'         => 'string',
		'name'         => 'string',
		'type'         => '?string',
		'image'        => 'int',
		'size'         => 'int',
		'hash'         => 'string',
		'storage'      => '?string',
		'storage_path' => '?string',
		'created'      => 'DateTime',
		'modified'     => 'DateTime',
		'author_id'    => '?int',
	];

	protected $_public;

	/**
	 * Tailles de miniatures autorisées, pour ne pas avoir 500 fichiers générés avec 500 tailles différentes
	 * @var array
	 */
	const ALLOWED_THUMB_SIZES = [200, 500, 1200];

	const LINK_USER = 'user_id';
	const LINK_TRANSACTION = 'transaction_id';
	const LINK_CONFIG = 'config';
	const LINK_WEB = 'web';

	const PATH_USER = 'users/%d';
	const PATH_TRANSACTION = 'accounting/transactions/%d';
	const PATH_CONFIG = 'config';
	const PATH_WEB = 'web/%s';

	const THUMB_CACHE_ID = 'file.thumb.%d.%d';

	public function selfCheck(): void
	{
		parent::selfCheck();
	}

	public function delete(): bool
	{
		$return = parent::delete();

		// clean up thumbs
		foreach (self::ALLOWED_THUMB_SIZES as $size)
		{
			Static_Cache::remove(sprintf(self::THUMB_CACHE_ID, $this->id(), $size));
		}

		return $return;
	}

	static protected function store(?string $path, string $name, string $source_path = null, $source_content = null): self
	{
		assert($path || $content);
146
147
148
149
150
151
152
153































































































































































































































































































		$name = preg_replace('/\s+/', '_', $file['name']);
		$name = preg_replace('/[^\d\w._-]/ui', '', $name);

		return self::store($path, $name, $file['tmp_name']);
	}

}





































































































































































































































































































|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485

		$name = preg_replace('/\s+/', '_', $file['name']);
		$name = preg_replace('/[^\d\w._-]/ui', '', $name);

		return self::store($path, $name, $file['tmp_name']);
	}


	/**
	 * Récupération du message d'erreur
	 * @param  integer $error Code erreur du $_FILE
	 * @return string Message d'erreur
	 */
	static public function getErrorMessage($error)
	{
		switch ($error)
		{
			case UPLOAD_ERR_INI_SIZE:
				return 'Le fichier excède la taille permise par la configuration du serveur.';
			case UPLOAD_ERR_FORM_SIZE:
				return 'Le fichier excède la taille permise par le formulaire.';
			case UPLOAD_ERR_PARTIAL:
				return 'L\'envoi du fichier a été interrompu.';
			case UPLOAD_ERR_NO_FILE:
				return 'Aucun fichier n\'a été reçu.';
			case UPLOAD_ERR_NO_TMP_DIR:
				return 'Pas de répertoire temporaire pour stocker le fichier.';
			case UPLOAD_ERR_CANT_WRITE:
				return 'Impossible d\'écrire le fichier sur le disque du serveur.';
			case UPLOAD_ERR_EXTENSION:
				return 'Une extension du serveur a interrompu l\'envoi du fichier.';
			default:
				return 'Erreur inconnue: ' . $error;
		}
	}

	public function url(?int $size = null): string
	{
		return self::getFileURL($this->id, $this->name, $this->hash, $size);
	}

	/**
	 * Renvoie l'URL vers un fichier
	 * @param  integer $id   Numéro du fichier
	 * @param  string  $name  Nom de fichier avec extension
	 * @param  integer $size Taille de la miniature désirée (pour les images)
	 * @return string        URL du fichier
	 */
	static public function getFileURL(int $id, string $name, string $hash, ?int $size = null): string
	{
		$url = sprintf('%sf/%s/%s?', WWW_URL, base_convert((int)$id, 10, 36), $name);

		if ($size)
		{
			$url .= self::_findNearestThumbSize($size) . 'px&';
		}

		$url .= substr($hash, 0, 10);

		return $url;
	}

	/**
	 * Renvoie la taille de miniature la plus proche de la taille demandée
	 * @param  integer $size Taille demandée
	 * @return integer       Taille possible
	 */
	static protected function _findNearestThumbSize($size)
	{
		$size = (int) $size;

		if (in_array($size, self::ALLOWED_THUMB_SIZES))
		{
			return $size;
		}

		foreach (self::ALLOWED_THUMB_SIZES as $s)
		{
			if ($s >= $size)
			{
				return $s;
			}
		}

		return max(self::ALLOWED_THUMB_SIZES);
	}

	/**
	 * Lier un fichier à un contenu
	 * @param  string $type       Type de contenu (constantes LINK_*)
	 * @param  integer $foreign_id ID du contenu lié
	 * @return boolean TRUE en cas de succès
	 */
	public function linkTo(string $type, int $foreign_id): bool
	{
		$db = DB::getInstance();
		static $types = [self::LINK_WEB, self::LINK_FILE, self::LINK_TRANSACTION, self::LINK_USER, self::LINK_CONFIG];

		if (!in_array($type, $types)) {
			throw new \InvalidArgumentException('Unknown file link type.');
		}

		if ($db->test('files_links', 'id = ?', $this->id())) {
			throw new \LogicException('This file is already linked to something else');
		}

		$sql = sprintf('INSERT OR IGNORE INTO files_links (id, %s) VALUES (?, ?);', $type);

		return $db->preparedQuery($sql, [$this->id, $foreign_id]);
	}

	public function getLinkedId(string $type): ?int
	{
		static $types = [self::LINK_WEB, self::LINK_FILE, self::LINK_TRANSACTION, self::LINK_USER, self::LINK_CONFIG];

		if (!in_array($type, $types)) {
			throw new \InvalidArgumentException('Unknown file link type.');
		}

		return DB::getInstance()->firstColumn(sprintf('SELECT %s FROM files_links WHERE id = %d;', $type, $this->id()));
	}

	/**
	 * Envoie le fichier au client HTTP
	 */
	public function serve(?Session $session = null): void
	{
		if (!$this->checkAccess($session)) {
			header('HTTP/1.1 403 Forbidden', true, 403);
			throw new UserException('Accès interdit');
			return;
		}

		$path = Files::callStorage('getPath', $this);
		$content = null === $path ? Files::callStorage('fetch', $this) : null;

		$this->_serve($session, $path, $content);
	}

	/**
	 * Envoie une miniature à la taille indiquée au client HTTP
	 */
	public function serveThumbnail(?Session $session = null, ?int $width = null): void
	{
		if (!$this->checkAccess($session)) {
			header('HTTP/1.1 403 Forbidden', true, 403);
			throw new UserException('Accès interdit');
			return;
		}

		if (!$this->image) {
			throw new UserException('Il n\'est pas possible de fournir une miniature pour un fichier qui n\'est pas une image.');
		}

		if (!$width) {
			$width = reset(self::ALLOWED_THUMB_SIZES);
		}

		if (!in_array($width, self::ALLOWED_THUMB_SIZES)) {
			throw new UserException('Cette taille de miniature n\'est pas autorisée.');
		}

		$cache_id = sprintf(self::THUMB_CACHE_ID, $this->id(), $width);
		$destination = Static_Cache::getPath($cache_id);

		// La miniature n'existe pas dans le cache statique, on la crée
		if (!Static_Cache::exists($cache_id))
		{
			try {
				if ($path = Files::callStorage('getPath', $file)) {
					(new Image($source))->resize($width)->save($destination);
				}
				elseif ($content = Files::callStorage('fetch', $file)) {
					Image::createFromBlob($content)->resize($width)->save($destination);
				}
				else {
					throw new \RuntimeException('Unable to fetch file');
				}
			}
			catch (\RuntimeException $e) {
				throw new UserException('Impossible de créer la miniature');
			}
		}

		$this->_serve($session, $path, null);
	}

	/**
	 * Servir un fichier local en HTTP
	 * @param  string $path Chemin vers le fichier local
	 * @param  string $type Type MIME du fichier
	 * @param  string $name Nom du fichier avec extension
	 * @param  integer $size Taille du fichier en octets (facultatif)
	 * @return boolean TRUE en cas de succès
	 */
	protected function _serve(?string $path, ?string $content): void
	{
		if ($this->isPublic()) {
			Utils::HTTPCache($this->hash, $this->datetime);
		}
		else {
			// Disable browser cache
			header('Pragma: private');
			header('Expires: -1');
			header('Cache-Control: private, must-revalidate, post-check=0, pre-check=0');
		}

		header(sprintf('Content-Type: %s', $this->type));
		header(sprintf('Content-Disposition: inline; filename="%s"', $this->name));

		// Utilisation de XSendFile si disponible
		if (null !== $path && ENABLE_XSENDFILE && isset($_SERVER['SERVER_SOFTWARE']))
		{
			if (stristr($_SERVER['SERVER_SOFTWARE'], 'apache') 
				&& function_exists('apache_get_modules') 
				&& in_array('mod_xsendfile', apache_get_modules()))
			{
				header('X-Sendfile: ' . $path);
				return;
			}
			else if (stristr($_SERVER['SERVER_SOFTWARE'], 'lighttpd'))
			{
				header('X-Sendfile: ' . $path);
				return;
			}
		}

		// Désactiver gzip
		if (function_exists('apache_setenv'))
		{
			@apache_setenv('no-gzip', 1);
		}

		@ini_set('zlib.output_compression', 'Off');

		header(sprintf('Content-Length: %d', $this->size));

		if (@ob_get_length()) {
			@ob_clean();
		}

		flush();

		if (null !== $path) {
			readfile($path);
		}
		else {
			echo $content;
		}
	}

	public function isPublic(): bool
	{
		if (null === $this->_public) {
			throw new \RuntimeException('_public is unset');
		}

		return $this->_public;
	}

	public function checkAccess(Session $session): bool
	{
		$link = DB::getInstance()->first('SELECT * FROM files_links WHERE id = ?;', $this->id());

		$this->_public = false;

		// Everyone has access to web content as long it's not draft (0)
		if ($link->{LINK_WEB} == 1) {
			$this->_public = true;
			return true;
		}
		elseif ($link->{LINK_WEB} == 0) {
			return false;
		}
		// Everyone has access to config files (logo etc.)
		else if ($link->{LINK_CONFIG}) {
			$this->_public = true;
			return true;
		}
		else if ($link->{LINK_TRANSACTION} && $session->canAccess('compta', Membres::DROIT_ACCES)) {
			return true;
		}
		// The user can access his own profile files
		else if ($link->{LINK_USER} && $link->{LINK_USER} == $session->getUser()->id) {
			return true;
		}
		// Only users able to manage users can see their profile files
		else if ($link->{LINK_USER} && $session->canAccess('membres', Membres::DROIT_ECRITURE)) {
			return true;
		}

		return $session->canAccess(Session::SECTION_DOCUMENTS, Membres::DROIT_ACCES);
	}
}

Modified src/include/lib/Garradin/Fichiers.php from [9ff6c36622] to [b460df65f5].

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
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
<?php

namespace Garradin;

use KD2\Graphics\Image;
use Garradin\Membres\Session;

class Fichiers
{
	public $id;
	public $nom;
	public $type;
	public $image;
	public $datetime;
	public $hash;
	public $taille;
	public $id_contenu;

	/**
	 * Tailles de miniatures autorisées, pour ne pas avoir 500 fichiers générés avec 500 tailles différentes
	 * @var array
	 */
	protected static $allowed_thumb_sizes = [200, 500];

	const LIEN_COMPTA = 'acc_transactions';
	const LIEN_WIKI = 'wiki_pages';
	const LIEN_MEMBRES = 'membres';

	/**
	 * Renvoie l'URL vers un fichier
	 * @param  integer $id   Numéro du fichier
	 * @param  string  $nom  Nom de fichier avec extension
	 * @param  integer $size Taille de la miniature désirée (pour les images)
	 * @return string        URL du fichier
	 */
	static public function _getURL(int $id, string $nom, string $hash, $size = false): string
	{
		$url = sprintf('%sf/%s/%s?', WWW_URL, base_convert((int)$id, 10, 36), $nom);

		if ($size)
		{
			$url .= self::_findThumbSize($size) . 'px&';
		}

		$url .= substr($hash, 0, 10);

		return $url;
	}

	/**
	 * Renvoie la taille de miniature la plus proche de la taille demandée
	 * @param  integer $size Taille demandée
	 * @return integer       Taille possible
	 */
	static protected function _findThumbSize($size)
	{
		$size = (int) $size;

		if (in_array($size, self::$allowed_thumb_sizes))
		{
			return $size;
		}

		foreach (self::$allowed_thumb_sizes as $s)
		{
			if ($s >= $size)
			{
				return $s;
			}
		}

		return max(self::$allowed_thumb_sizes);
	}

	/**
	 * Constructeur de l'objet pour un fichier
	 * @param integer $id Numéro unique du fichier
	 * @param $data array|object File data to populate object
	 */
	public function __construct(int $id, $data = null)
	{
		if (is_null($data))
		{
			$data = DB::getInstance()->first('SELECT fichiers.*, fc.hash, fc.taille,
				strftime(\'%s\', datetime) AS datetime
				FROM fichiers INNER JOIN fichiers_contenu AS fc ON fc.id = fichiers.id_contenu
				WHERE fichiers.id = ?;', (int)$id);
		}

		if (!$data)
		{
			throw new \InvalidArgumentException('Ce fichier n\'existe pas.');
		}

		foreach ((array)$data as $key => $value)
		{
			$this->$key = $value;
		}
	}

	/**
	 * Renvoie l'adresse d'accès au fichier
	 * @param  boolean $size Taille éventuelle de la miniature demandée
	 * @return string        URL d'accès au fichier
	 */
	public function getURL($size = false)
	{
		return self::_getURL($this->id, $this->nom, $this->hash, $size);
	}

	/**
	 * Lier un fichier à un contenu
	 * @param  string $type       Type de contenu (constantes LIEN_*)
	 * @param  integer $foreign_id ID du contenu lié
	 * @return boolean TRUE en cas de succès
	 */
	public function linkTo($type, $foreign_id)
	{
		$db = DB::getInstance();
		$check = [self::LIEN_MEMBRES, self::LIEN_WIKI, self::LIEN_COMPTA];

		if (!in_array($type, $check))
		{
			throw new \LogicException('Type de lien de fichier inconnu.');
		}

		// Vérifier que le fichier n'est pas déjà lié à un autre type
		$query = [];

		foreach ($check as $check_type)
		{
			// Ne pas chercher dans le type qu'on veut lier
			if ($check_type == $type)
			{
				continue;
			}

			$query[] = sprintf('SELECT 1 FROM fichiers_%s WHERE fichier = %d', $check_type, $this->id);
		}

		$query = implode(' UNION ', $query) . ';';

		if ($db->firstColumn($query))
		{
			throw new \LogicException('Ce fichier est déjà lié à un autre contenu : ' . $check_type);
		}

		return $db->preparedQuery('INSERT OR IGNORE INTO fichiers_' . $type . ' (fichier, id) VALUES (?, ?);',
			[(int)$this->id, (int)$foreign_id]);
	}

	public function getLinkedId(string $type)
	{
		$check = [self::LIEN_MEMBRES, self::LIEN_WIKI, self::LIEN_COMPTA];

		if (!in_array($type, $check))
		{
			throw new \LogicException('Type de lien de fichier inconnu.');
		}


		return DB::getInstance()->firstColumn(sprintf('SELECT id FROM fichiers_%s WHERE fichier = %d;', $type, $this->id));
	}

	public function isPublic(&$wiki = null): bool
	{
		$config = Config::getInstance();

		if ($config->get('image_fond') == $this->id)
		{
			return true;
		}

		// On regarde déjà si le fichier n'est pas lié au wiki
		$query = sprintf('SELECT wp.droit_lecture FROM fichiers_%s AS link
			INNER JOIN wiki_pages AS wp ON wp.id = link.id
			WHERE link.fichier = ? LIMIT 1;', self::LIEN_WIKI);
		$wiki = DB::getInstance()->firstColumn($query, (int)$this->id);

		// Page wiki publique, aucune vérification à faire, seul cas d'accès à un fichier en dehors de l'espace admin
		if ($wiki !== false && $wiki == Wiki::LECTURE_PUBLIC)
		{
			return true;
		}

		return false;
	}

	/**
	 * Vérifie que l'utilisateur a bien le droit d'accéder à ce fichier
	 * @param  mixed   $user Tableau contenant les infos sur l'utilisateur connecté, provenant de Session::getUser, ou false
	 * @return boolean       TRUE si l'utilisateur a le droit d'accéder au fichier, sinon FALSE
	 */
	public function checkAccess(Session $session, bool $require_admin = false)
	{
		$wiki = null;

		if ($this->isPublic($wiki) && !$require_admin) {
			return true;
		}

		// Pas d'utilisateur connecté, pas d'accès aux fichiers de l'espace admin
		if (!$session->isLogged())
		{
			return false;
		}

		$user = $session->getUser();

		if ($wiki !== false)
		{
			// S'il n'a même pas droit à accéder au wiki c'est mort
			if (!$session->canAccess('wiki', Membres::DROIT_ACCES))
			{
				return false;
			}

			// On renvoie à l'objet Wiki pour savoir si l'utilisateur a le droit de lire ce fichier
			$_w = new Wiki;
			$_w->setRestrictionCategorie($user->id_categorie, $user->droit_wiki);
			return $require_admin ? $_w->canWritePage($wiki) : $_w->canReadPage($wiki);
		}

		$level = $require_admin ? Membres::DROIT_ADMIN : Membres::DROIT_ACCES;

		$db = DB::getInstance();

		// On regarde maintenant si le fichier est lié à la compta
		$query = sprintf('SELECT 1 FROM fichiers_%s WHERE fichier = ? LIMIT 1;', self::LIEN_COMPTA);
		$compta = $db->firstColumn($query, (int)$this->id);

		if ($compta)
		{
			// OK si accès à la compta
			return $session->canAccess('compta', $level);
		}

		// Enfin, si le fichier est lié à un membre
		$query = sprintf('SELECT id FROM fichiers_%s WHERE fichier = ? LIMIT 1;', self::LIEN_MEMBRES);
		$membre = $db->firstColumn($query, (int)$this->id);

		if ($membre !== false)
		{
			// De manière évidente, l'utilisateur a le droit d'accéder aux fichiers liés à son profil
			if ((int)$membre == $user->id)
			{
				return true;
			}

			// Pour voir les fichiers des membres il faut pouvoir les gérer
			if ($level == Membres::DROIT_ACCES) {
				$level = Membres::DROIT_ECRITURE;
			}

			if ($session->canAccess('membres', $level))
			{
				return true;
			}
		}

		return false;
	}

	/**
	 * Supprime le fichier
	 * @return boolean TRUE en cas de succès
	 */
	public function remove()
	{
		$db = DB::getInstance();
		$db->begin();
		$db->delete('fichiers_' . self::LIEN_COMPTA, 'fichier = ?', (int)$this->id);
		$db->delete('fichiers_' . self::LIEN_WIKI, 'fichier = ?', (int)$this->id);
		$db->delete('fichiers_' . self::LIEN_MEMBRES, 'fichier = ?', (int)$this->id);

		$db->delete('fichiers', 'id = ?', (int)$this->id);

		// Suppression du contenu s'il n'est pas utilisé par un autre fichier
		if (!$db->firstColumn('SELECT 1 FROM fichiers WHERE id_contenu = ? AND id != ? LIMIT 1;', 
			(int)$this->id_contenu, (int)$this->id))
		{
			$db->delete('fichiers_contenu', 'id = ?', (int)$this->id_contenu);
		}

		$cache_id = 'fichiers.' . $this->id_contenu;
		
		Static_Cache::remove($cache_id);

		foreach (self::$allowed_thumb_sizes as $size)
		{
			Static_Cache::remove($cache_id . '.thumb.' . (int)$size);
		}

		return $db->commit();
	}

	/**
	 * Renvoie le chemin vers le fichier local en cache, et le crée s'il n'existe pas
	 * @return string Chemin local
	 */
	protected function getFilePathFromCache()
	{
		// Le cache est géré par ID contenu, pas ID fichier, pour minimiser l'espace disque utilisé
		$cache_id = 'fichiers.' . $this->id_contenu;

		// Le fichier n'existe pas dans le cache statique, on l'enregistre
		if (!Static_Cache::exists($cache_id))
		{
			$blob = DB::getInstance()->openBlob('fichiers_contenu', 'contenu', (int)$this->id_contenu);
			Static_Cache::storeFromPointer($cache_id, $blob);
			fclose($blob);
		}

		return Static_Cache::getPath($cache_id);
	}

	/**
	 * Envoie le fichier au client HTTP
	 * @return void
	 */
	public function serve()
	{
		return $this->_serve($this->getFilePathFromCache(), $this->type, ($this->image ? false : $this->nom), $this->taille, $this->isPublic());
	}

	/**
	 * Envoie une miniature à la taille indiquée au client HTTP
	 * @return void
	 */
	public function serveThumbnail($width = null)
	{
		if (!$this->image)
		{
			throw new UserException('Il n\'est pas possible de fournir une miniature pour un fichier qui n\'est pas une image.');
		}

		if (!$width)
		{
			$width = reset(self::$allowed_thumb_sizes);
		}

		if (!in_array($width, self::$allowed_thumb_sizes))
		{
			throw new UserException('Cette taille de miniature n\'est pas autorisée.');
		}

		$cache_id = 'fichiers.' . $this->id_contenu . '.thumb.' . (int)$width;
		$path = Static_Cache::getPath($cache_id);

		// La miniature n'existe pas dans le cache statique, on la crée
		if (!Static_Cache::exists($cache_id))
		{
			$source = $this->getFilePathFromCache();

			try {
				(new Image($source))->resize($width)->save($path);
			}
			catch (\RuntimeException $e) {
				throw new UserException('Impossible de créer la miniature');
			}
		}

		return $this->_serve($path, $this->type);
	}

	/**
	 * Servir un fichier local en HTTP
	 * @param  string $path Chemin vers le fichier local
	 * @param  string $type Type MIME du fichier
	 * @param  string $name Nom du fichier avec extension
	 * @param  integer $size Taille du fichier en octets (facultatif)
	 * @return boolean TRUE en cas de succès
	 */
	protected function _serve($path, $type, $name = false, $size = null, bool $public = false)
	{
		if ($public) {
			Utils::HTTPCache($this->hash, $this->datetime);
		}
		else {
			// Désactiver le cache
			header('Pragma: public');
			header('Expires: -1');
			header('Cache-Control: public, must-revalidate, post-check=0, pre-check=0');
		}

		header('Content-Type: '.$type);

		if ($name)
		{
			header('Content-Disposition: attachment; filename="' . $name . '"');
		}

		// Utilisation de XSendFile si disponible
		if (ENABLE_XSENDFILE && isset($_SERVER['SERVER_SOFTWARE']))
		{
			if (stristr($_SERVER['SERVER_SOFTWARE'], 'apache') 
				&& function_exists('apache_get_modules') 
				&& in_array('mod_xsendfile', apache_get_modules()))
			{
				header('X-Sendfile: ' . $path);
				return true;
			}
			else if (stristr($_SERVER['SERVER_SOFTWARE'], 'lighttpd'))
			{
				header('X-Sendfile: ' . $path);
				return true;
			}
		}

		// Désactiver gzip
		if (function_exists('apache_setenv'))
		{
			@apache_setenv('no-gzip', 1);
		}

		@ini_set('zlib.output_compression', 'Off');

		if ($size)
		{
			header('Content-Length: '. (int)$size);
		}

		if (@ob_get_length()) {
			@ob_clean();
		}

		flush();

		// Sinon on envoie le fichier à la mano
		return readfile($path);
	}

	/**
	 * Vérifie si le hash fourni n'est pas déjà stocké
	 * Utile pour par exemple reconnaître un ficher dont le contenu est déjà stocké, et éviter un nouvel upload
	 * @param  string $hash Hash SHA1
	 * @return boolean      TRUE si le hash est déjà présent dans fichiers_contenu, FALSE sinon
	 */









<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<







1
2
3
4
5
6
7
8
9






































































































































































































































































































































































































































10
11
12
13
14
15
16
<?php

namespace Garradin;

use KD2\Graphics\Image;
use Garradin\Membres\Session;

class Fichiers
{







































































































































































































































































































































































































































	/**
	 * Vérifie si le hash fourni n'est pas déjà stocké
	 * Utile pour par exemple reconnaître un ficher dont le contenu est déjà stocké, et éviter un nouvel upload
	 * @param  string $hash Hash SHA1
	 * @return boolean      TRUE si le hash est déjà présent dans fichiers_contenu, FALSE sinon
	 */
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
			$a = $db->quote($a);
		});

		$query = sprintf('SELECT hash, 1 FROM fichiers_contenu WHERE hash IN (%s);', 
			implode(', ', $list));
		return $db->getAssoc($query);
	}

	/**
	 * Récupération du message d'erreur
	 * @param  integer $error Code erreur du $_FILE
	 * @return string Message d'erreur
	 */
	static public function getErrorMessage($error)
	{
		switch ($error)
		{
			case UPLOAD_ERR_INI_SIZE:
				return 'Le fichier excède la taille permise par la configuration du serveur.';
			case UPLOAD_ERR_FORM_SIZE:
				return 'Le fichier excède la taille permise par le formulaire.';
			case UPLOAD_ERR_PARTIAL:
				return 'L\'envoi du fichier a été interrompu.';
			case UPLOAD_ERR_NO_FILE:
				return 'Aucun fichier n\'a été reçu.';
			case UPLOAD_ERR_NO_TMP_DIR:
				return 'Pas de répertoire temporaire pour stocker le fichier.';
			case UPLOAD_ERR_CANT_WRITE:
				return 'Impossible d\'écrire le fichier sur le disque du serveur.';
			case UPLOAD_ERR_EXTENSION:
				return 'Une extension du serveur a interrompu l\'envoi du fichier.';
			default:
				return 'Erreur inconnue: ' . $error;
		}
	}



	/**
	 * Envoie un fichier déjà stocké
	 * 
	 * @param  string $name Nom du fichier
	 * @param  string $hash Hash SHA1 du contenu du fichier







<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<







35
36
37
38
39
40
41





























42
43
44
45
46
47
48
			$a = $db->quote($a);
		});

		$query = sprintf('SELECT hash, 1 FROM fichiers_contenu WHERE hash IN (%s);', 
			implode(', ', $list));
		return $db->getAssoc($query);
	}































	/**
	 * Envoie un fichier déjà stocké
	 * 
	 * @param  string $name Nom du fichier
	 * @param  string $hash Hash SHA1 du contenu du fichier
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
			'type'       =>	$file->type,
			'image'      =>	(int)$file->image,
		]);

		return new Fichiers($db->lastInsertRowID());
	}

	/**
	 * Récupère la liste des fichiers liés à une ressource
	 * 
	 * @param  string  $type    Type de ressource
	 * @param  integer $id      Numéro de ressource
	 * @param  boolean|null $images  TRUE pour retourner seulement les images,
	 * FALSE pour retourner les fichiers sans images, NULL pour tout retourner
	 * @return array          Liste des fichiers
	 */
	static public function listLinkedFiles($type, $id, $images = null)
	{
		$check = [self::LIEN_MEMBRES, self::LIEN_WIKI, self::LIEN_COMPTA];

		if (!in_array($type, $check))
		{
			throw new \LogicException('Type de lien de fichier inconnu.');
		}

		$images = is_null($images) ? '' : ' AND image = ' . (int)$images;

		$query = sprintf('SELECT fichiers.*, c.hash, c.taille
			FROM fichiers 
			INNER JOIN fichiers_%s AS fwp ON fwp.fichier = fichiers.id
			INNER JOIN fichiers_contenu AS c ON c.id = fichiers.id_contenu
			WHERE fwp.id = ? %s
			ORDER BY fichiers.nom COLLATE NOCASE;', $type, $images);

		$files = DB::getInstance()->get($query, (int)$id);

		foreach ($files as &$file)
		{
			$file->url = self::_getURL($file->id, $file->nom, $file->hash);
			$file->thumb = $file->image ? self::_getURL($file->id, $file->nom, $file->hash, 200) : false;
		}

		return $files;
	}

	static public function deleteLinkedFiles($type, int $id)
	{
		static $check = [self::LIEN_MEMBRES, self::LIEN_WIKI, self::LIEN_COMPTA];

		if (!in_array($type, $check))
		{
			throw new \LogicException('Type de lien de fichier inconnu.');
		}

		$files = DB::getInstance()->delete('fichiers_' . $type, 'id = ?', $id);
		return self::deleteUnlinkedFiles();
	}

	static public function deleteUnlinkedFiles()
	{
		static $all = [self::LIEN_MEMBRES, self::LIEN_WIKI, self::LIEN_COMPTA];
		$id_background = Config::getInstance()->get('image_fond');

		$list = DB::getInstance()->iterate(sprintf('SELECT f.id, f.id_contenu FROM fichiers f
			LEFT JOIN fichiers_%s a ON a.fichier = f.id
			LEFT JOIN fichiers_%s b ON b.fichier = f.id
			LEFT JOIN fichiers_%s c ON c.fichier = f.id
			WHERE a.id IS NULL AND b.id IS NULL AND c.id IS NULL;',
			self::LIEN_MEMBRES,
			self::LIEN_WIKI,
			self::LIEN_COMPTA));

		foreach ($list as $file) {
			if ($file->id == $id_background) { // FIXME: want to use something cleaner here!
				continue;
			}

			$f = new Fichiers($file->id, (array) $file);
			$f->remove();
		}
	}

	/**
	 * Enlève d'une liste de fichiers ceux qui sont mentionnés dans un texte wiki
	 * @param  array $files Liste de fichiers
	 * @param  string $text  texte wiki
	 * @return array        Un tableau qui ne contient pas les fichiers mentionnés dans $text
	 */







<
<
|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<







67
68
69
70
71
72
73


74







































































75
76
77
78
79
80
81
			'type'       =>	$file->type,
			'image'      =>	(int)$file->image,
		]);

		return new Fichiers($db->lastInsertRowID());
	}












































































	/**
	 * Enlève d'une liste de fichiers ceux qui sont mentionnés dans un texte wiki
	 * @param  array $files Liste de fichiers
	 * @param  string $text  texte wiki
	 * @return array        Un tableau qui ne contient pas les fichiers mentionnés dans $text
	 */

Modified src/include/lib/Garradin/Files/Files.php from [9ec11674dc] to [c115561b4c].

24
25
26
27
28
29
30































31
32
33
34
35
36
37
		$to = get_class(__NAMESPACE__ . '\\Backend\\' . $to);

		foreach ($res as $file) {
			$from_path = call_user_func([$from, 'path'], $file);
			call_user_func([$to, 'store'], $file, $from_path);
		}
	}
































	static public function generatePathsIndex(): void
	{
		$all = DB::getInstance()->getAssoc('SELECT path, path FROM files GROUP BY path;');
		$paths = [];

		foreach ($all as $path) {







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







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
		$to = get_class(__NAMESPACE__ . '\\Backend\\' . $to);

		foreach ($res as $file) {
			$from_path = call_user_func([$from, 'path'], $file);
			call_user_func([$to, 'store'], $file, $from_path);
		}
	}

	static public function deleteOrphanFiles()
	{
		$db = DB::getInstance();
		$sql = 'SELECT f.* FROM files f LEFT JOIN files_links l ON f.id = l.id WHERE l.id IS NULL;';

		foreach ($db->iterate($sql) as $file) {
			$f = new Fichiers($file->id, (array) $file);
			$f->remove();
		}

		// Remove any left-overs
		$db->exec('DELETE FROM files_contents WHERE hash NOT IN (SELECT DISTINCT hash FROM files);');
	}

	static public function deleteLinkedFiles(string $type, ?int $value = null)
	{
		foreach (self::iterateLinkedTo($type, $value) as $file) {
			$file->delete();
		}

		self::deleteOrphanFiles();
	}

	static public function iterateLinkedTo(string $type, ?int $value = null)
	{
		$where = $value ? sprintf('l.%s = %d', $value) : sprintf('l.%s IS NOT NULL');
		$sql = sprintf('SELECT f.* FROM @TABLE f INNER JOIN files_links l ON l.id = f.id WHERE %s;', $where);

		return EM::getInstance(File::class)->iterate($sql);
	}

	static public function generatePathsIndex(): void
	{
		$all = DB::getInstance()->getAssoc('SELECT path, path FROM files GROUP BY path;');
		$paths = [];

		foreach ($all as $path) {

Modified src/include/lib/Garradin/Files/Storage/SQLite.php from [09143891cd] to [e6d2cf42a4].

47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
	static public function list(string $path): ?array
	{
		return null;
	}

	static public function getPath(File $file): ?string
	{
		return null;
	}

	static public function display(File $file): void
	{
		readfile(self::getFilePathFromCache($file));
	}








|







47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
	static public function list(string $path): ?array
	{
		return null;
	}

	static public function getPath(File $file): ?string
	{
		return self::_getFilePathFromCache($file);
	}

	static public function display(File $file): void
	{
		readfile(self::getFilePathFromCache($file));
	}