Overview
Comment:Implement listing from filesystem
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | dev
Files: files | file ages | folders
SHA3-256: da1614a872f324b15c392b6507e168674a4758508dba80d89274d5aeaa2d90e2
User & Date: bohwaz on 2021-02-01 04:13:04
Other Links: branch diff | manifest | tags
Context
2021-02-01
04:29
Update file if required when it is coming from filesystem check-in: 010d704636 user: bohwaz tags: dev
04:13
Implement listing from filesystem check-in: da1614a872 user: bohwaz tags: dev
02:37
Progress on web management check-in: 749e52c325 user: bohwaz tags: dev
Changes

Modified src/include/lib/Garradin/Entities/Files/File.php from [af7e4db8bd] to [243af884b0].

225
226
227
228
229
230
231
232
233
234
235
236
237
238
239

		$file->store($source_path, $source_content);
		$file->save();

		return $file;
	}

	static protected function create(string $name, string $context, ?string $context_ref, string $source_path = null, string $source_content = null): self
	{
		if (isset($source_path, $source_content)) {
			throw new \InvalidArgumentException('Either source path or source content should be set but not both');
		}

		$finfo = \finfo_open(\FILEINFO_MIME_TYPE);
		$file = new self;







|







225
226
227
228
229
230
231
232
233
234
235
236
237
238
239

		$file->store($source_path, $source_content);
		$file->save();

		return $file;
	}

	static public function create(string $name, string $context, ?string $context_ref, string $source_path = null, string $source_content = null): self
	{
		if (isset($source_path, $source_content)) {
			throw new \InvalidArgumentException('Either source path or source content should be set but not both');
		}

		$finfo = \finfo_open(\FILEINFO_MIME_TYPE);
		$file = new self;
647
648
649
650
651
652
653



654



655




656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
			case self::CONTEXT_USER:
				return $session->canAccess($session::SECTION_USERS, $session::ACCESS_WRITE);
		}

		return false;
	}




	public function getPathForContext(string $context, $value): string



	{




		return rtrim($context . '/' . $value, '/');
	}

	public function path(): string
	{
		return self::getPathForContext($this->context, $this->context_ref) . '/' . $this->name;
	}

	/**
	 * Create a file in DB from an existing file in the local filesysteme
	 */
	static public function createFromExisting(string $path, string $root): File
	{
		$ctx = self::getContextFromPath($path);
		$fullpath = $root . '/' . $path;

		$file = File::create($name, $ctx[0], $ctx[1], $fullpath);

		$file->set('hash', sha1_file($fullpath));
		$file->set('size', filesize($fullpath));
		$file->set('modified', filemtime($fullpath));
		$file->set('created', filemtime($fullpath));

		$file->save();

		return $file;
	}

	static public function getContextFromPath(string $path): array
	{
		$context = strtok($this->path, '/');
		$value = strtok('');

		return [$context, $value];
	}

	public function checkContext(string $context, $ref): bool
	{
		return ($this->context === $context) && ($this->context_ref == $ref);
	}

	public function parent(): ?File
	{







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




|





|

|
|

|


|
|
|






<
<
<
<
<
<
<
<







647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693








694
695
696
697
698
699
700
			case self::CONTEXT_USER:
				return $session->canAccess($session::SECTION_USERS, $session::ACCESS_WRITE);
		}

		return false;
	}

	static public function getPath(string $context, ?string $ref, ?string $name = null): string
	{
		$path = $context;

		if ($ref) {
			$path .= '/' . $ref;
		}

		if ($name) {
			$path .= '/' . $name;
		}

		return $path;
	}

	public function path(): string
	{
		return self::getPath($this->context, $this->context_ref, $this->name);
	}

	/**
	 * Create a file in DB from an existing file in the local filesysteme
	 */
	static public function createFromExisting(string $path, string $root, ?\SplFileInfo $info = null): File
	{
		list($context, $ref, $name) = self::validatePath($path);
		$fullpath = $root . DIRECTORY_SEPARATOR . $path;

		$file = File::create($name, $context, $ref, $fullpath);

		$file->set('hash', sha1_file($fullpath));
		$file->set('size', $info ? $info->getSize() : filesize($fullpath));
		$file->set('modified', new \DateTime('@' . ($info ? $info->getMTime() : filemtime($fullpath))));
		$file->set('created', $file->get('modified'));

		$file->save();

		return $file;
	}









	public function checkContext(string $context, $ref): bool
	{
		return ($this->context === $context) && ($this->context_ref == $ref);
	}

	public function parent(): ?File
	{
758
759
760
761
762
763
764
765
766
767




768
769
770
771


772
773
774
775
776
777
778



779

			self::FILE_TYPE_ENCRYPTED,
			self::FILE_TYPE_HTML,
		];

		return in_array($this->type, $types);
	}

	static public function validatePath(string $path)
	{
		$path = explode('/', $path);





		if (!array_key_exists($path[0], self::CONTEXTS_NAMES)) {
			throw new ValidationException('Chemin invalide');
		}



		foreach ($path as $part) {
			if (!preg_match('!^[\w\d_-]+(?:\.[\w\d_-]+)*$!i', $part)) {
				throw new ValidationException('Chemin invalide');
			}
		}
	}



}








|


>
>
>
>




>
>






|
>
>
>
|
>
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
			self::FILE_TYPE_ENCRYPTED,
			self::FILE_TYPE_HTML,
		];

		return in_array($this->type, $types);
	}

	static public function validatePath(string $path): array
	{
		$path = explode('/', $path);

		if (count($path) < 2) {
			throw new ValidationException('Invalid file path');
		}

		if (!array_key_exists($path[0], self::CONTEXTS_NAMES)) {
			throw new ValidationException('Chemin invalide');
		}

		$context = array_shift($path);

		foreach ($path as $part) {
			if (!preg_match('!^[\w\d_-]+(?:\.[\w\d_-]+)*$!i', $part)) {
				throw new ValidationException('Chemin invalide');
			}
		}

		$name = array_pop($path);
		$ref = implode('/', $path);
		return [$context, $ref ?: null, $name];
	}
}

Modified src/include/lib/Garradin/Files/Files.php from [23ffdcfb06] to [729063f6af].

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
		if ($id) {
			Utils::redirect(ADMIN_URL . 'files/file.php?id=' . $id);
		}

		return null;
	}

	static public function getWithNameAndContext(string $name, string $context): ?File
	{
		return EM::findOne(File::class, 'SELECT * FROM files WHERE name = ? AND context = ?;', $name, $context);
	}

	static public function listNamesForContext(string $context): array
	{
		return EM::getInstance(File::class)->DB()->getAssoc('SELECT id, name FROM files WHERE context = ? ORDER BY name;', $context);
	}

	static public function list(string $context, ?string $ref): array
	{
		if (!array_key_exists($context, File::CONTEXTS_NAMES)) {
			throw new \InvalidArgumentException('Invalid context');
		}

		return self::callStorage('list', $context, $ref);
	}







<
<
<
<
<
<
<
<
<
<
|







28
29
30
31
32
33
34










35
36
37
38
39
40
41
42
		if ($id) {
			Utils::redirect(ADMIN_URL . 'files/file.php?id=' . $id);
		}

		return null;
	}











	static public function list(string $context, ?string $ref = null): array
	{
		if (!array_key_exists($context, File::CONTEXTS_NAMES)) {
			throw new \InvalidArgumentException('Invalid context');
		}

		return self::callStorage('list', $context, $ref);
	}
210
211
212
213
214
215
216





























217
218
219
220
221
222
223
		return iterator_to_array(self::iterateLinkedTo($context, $value));
	}

	static public function get(int $id): ?File
	{
		return EM::findOneById(File::class, $id);
	}






























	static public function serveFromQueryString(): void
	{
		$id = isset($_GET['id']) ? $_GET['id'] : null;
		$filename = !empty($_GET['file']) ? $_GET['file'] : null;

		$size = null;







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







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
		return iterator_to_array(self::iterateLinkedTo($context, $value));
	}

	static public function get(int $id): ?File
	{
		return EM::findOneById(File::class, $id);
	}

	static public function getFromPath(string $path): ?File
	{
		list($context, $ref, $name) = File::validatePath($path);
		return self::getFromContext($context, $ref, $name);
	}

	static public function getFromContext(string $context, ?string $context_ref, ?string $name): ?File
	{
		$conditions = ['context = ?'];
		$params = [$context];

		if (null === $context_ref) {
			$conditions[] = 'context_ref IS NULL';
		}
		else {
			$conditions[] = 'context_ref = ?';
			$params[] = $context;
		}

		if (null !== $name) {
			$conditions[] = 'name = ?';
			$params[] = $name;
		}

		$conditions = implode(' AND ', $conditions);

		return EM::findOne(File::class, sprintf('SELECT * FROM @TABLE WHERE %s', $conditions), ...$params);
	}

	static public function serveFromQueryString(): void
	{
		$id = isset($_GET['id']) ? $_GET['id'] : null;
		$filename = !empty($_GET['file']) ? $_GET['file'] : null;

		$size = null;

Modified src/include/lib/Garradin/Files/Storage/FileSystem.php from [764ebd35f3] to [26061d1f86].

1
2
3
4

5
6
7
8
9
10
11
<?php

namespace Garradin\Files\Storage;


use Garradin\Entities\Files\File;
use Garradin\Utils;

use const Garradin\FILE_STORAGE_CONFIG;

/**
 * This class provides storage in the file system




>







1
2
3
4
5
6
7
8
9
10
11
12
<?php

namespace Garradin\Files\Storage;

use Garradin\Files\Files;
use Garradin\Entities\Files\File;
use Garradin\Utils;

use const Garradin\FILE_STORAGE_CONFIG;

/**
 * This class provides storage in the file system
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
			return copy($path, $target);
		}
		else {
			return file_put_contents($target, $content) === false ? false : true;
		}
	}

	static public function list(string $path): array
	{


		$path = self::_getRoot() . ($path ? DIRECTORY_SEPARATOR . $path : '') . DIRECTORY_SEPARATOR . '*';
		$files = glob($path);
		$list = [];





		foreach ($files as $file) {


			throw new \Exception('FIXME');

		}


















		return $list;
	}

	static public function getPath(File $file): ?string
	{
		return self::_getRoot() . DIRECTORY_SEPARATOR . $file->path();
	}








|

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







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
			return copy($path, $target);
		}
		else {
			return file_put_contents($target, $content) === false ? false : true;
		}
	}

	static public function list(string $context, ?string $context_ref): array
	{
		$path = File::getPath($context, $context_ref);
		$path = self::_getRoot() . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $path);

		$directories = $files = [];

		foreach (new \FilesystemIterator($path, \FilesystemIterator::SKIP_DOTS) as $file) {
			if ($file->isDir()) {
				$directories[] = $file->getFilename();
				continue;
			}

			$relative_path = str_replace(self::_getRoot() . DIRECTORY_SEPARATOR, '', $file->getPathname());
			$relative_path = str_replace(DIRECTORY_SEPARATOR, '/', $relative_path);

			$file_object = Files::getFromPath($relative_path);

			if (!$file_object) {
				$file_object = File::createFromExisting($relative_path, self::_getRoot(), $file);
			}
			// Update metadata
			else if ($file->getMTime() > $file_object->modified->getTimestamp()) {
				$file_object->modified = new \DateTime('@' . $file->getMTime());
				$file_object->hash = self::hash($file_object);
				$file_object->size = $file->getSize();
				$file_object->save();
			}

			$files[] = $file_object;
		}

		usort($files, function ($a, $b) {
			return strnatcasecmp($a->name, $b->name) > 0 ? 1 : -1;
		});

		return $directories + $files;
	}

	static public function getPath(File $file): ?string
	{
		return self::_getRoot() . DIRECTORY_SEPARATOR . $file->path();
	}

99
100
101
102
103
104
105















106
107
108
109
110
111
112
	static public function move(File $old_file, File $new_file): bool
	{
		$target = self::getPath($new_file);
		self::ensureDirectoryExists(dirname($target));

		return rename(self::getPath($old_file), $target);
	}
















	static public function getTotalSize(): int
	{
		if (null !== self::$_size) {
			return self::$_size;
		}








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







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
	static public function move(File $old_file, File $new_file): bool
	{
		$target = self::getPath($new_file);
		self::ensureDirectoryExists(dirname($target));

		return rename(self::getPath($old_file), $target);
	}

	static public function exists(string $context, ?string $context_ref, string $name): bool
	{
		return (bool) file_exists(File::getPath($context, $context_ref, $name));
	}

	static public function modified(File $file): ?int
	{
		return filemtime(self::getPath($file)) ?: null;
	}

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

	static public function getTotalSize(): int
	{
		if (null !== self::$_size) {
			return self::$_size;
		}

124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
	}

	static public function getQuota(): int
	{
		return disk_total_space(self::_getRoot());
	}

	static public function cleanup(): void
	{
		// FIXME
	}

/*
	static public function sync(): void
	{







|







166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
	}

	static public function getQuota(): int
	{
		return disk_total_space(self::_getRoot());
	}

	static public function sync(): void
	{
		// FIXME
	}

/*
	static public function sync(): void
	{

Modified src/include/lib/Garradin/Files/Storage/SQLite.php from [a5656d7d88] to [b3ac1f3c59].

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
		Static_Cache::remove($cache_id);

		return $db->delete('files_contents', 'hash = ?', (int)$file->hash);
	}

	static public function move(File $old_file, File $new_file): bool
	{

		return true;
	}
















	static public function getTotalSize(): int
	{
		return (int) DB::getInstance()->firstColumn('SELECT SUM(size) FROM files_contents;');
	}

	static public function getQuota(): int
	{
		return disk_total_space(dirname(DB_FILE));
	}

	static public function cleanup(): void
	{
		$db = DB::getInstance();

		$sql = 'SELECT c.id, c.hash FROM files_contents c LEFT JOIN files f ON f.hash = c.hash WHERE f.hash IS NULL;';
		$ids = [];

		foreach ($db->iterate($sql) as $row) {







>


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








|


|







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
		Static_Cache::remove($cache_id);

		return $db->delete('files_contents', 'hash = ?', (int)$file->hash);
	}

	static public function move(File $old_file, File $new_file): bool
	{
		// No need to do anything here
		return true;
	}

	static public function exists(string $context, ?string $context_ref, string $name): bool
	{
		return true;
	}

	static public function modified(File $file): ?int
	{
		return null;
	}

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

	static public function getTotalSize(): int
	{
		return (int) DB::getInstance()->firstColumn('SELECT SUM(size) FROM files_contents;');
	}

	static public function getQuota(): int
	{
		return disk_total_space(dirname(DATA_ROOT));
	}

	static public function sync(): void
	{
		$db = DB::getInstance();

		$sql = 'SELECT c.id, c.hash FROM files_contents c LEFT JOIN files f ON f.hash = c.hash WHERE f.hash IS NULL;';
		$ids = [];

		foreach ($db->iterate($sql) as $row) {

Modified src/include/lib/Garradin/Files/Storage/StorageInterface.php from [8e424ec07e] to [59274f5464].

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
	static public function delete(File $file): bool;

	/**
	 * Moves a file to a new path, when its name or path has changed
	 */
	static public function move(File $old_file, File $new_file): bool;
















	/**
	 * Return total size of used space by files stored in this backed
	 */
	static public function getTotalSize(): int;

	/**
	 * Return available disk space
	 * This will only be called if FILE_STORAGE_QUOTA constant is null
	 */
	static public function getQuota(): int;

	/**
	 * This is called periodically to:
	 * - delete files from the storage that are no longer in the DB
	 */
	static public function cleanup(): void;

	/**
	 * Delete all stored content in this backend
	 */
	static public function reset(): void;

	/**







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












|
<

|







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
	static public function delete(File $file): bool;

	/**
	 * Moves a file to a new path, when its name or path has changed
	 */
	static public function move(File $old_file, File $new_file): bool;

	/**
	 * Returns TRUE if the file exists
	 */
	static public function exists(string $context, ?string $context_ref, string $name): bool;

	/**
	 * Returns file modification time as UNIX timestamp
	 */
	static public function modified(File $file): ?int;

	/**
	 * Returns SHA1 hash of the file content
	 */
	static public function hash(File $file): ?string;

	/**
	 * Return total size of used space by files stored in this backed
	 */
	static public function getTotalSize(): int;

	/**
	 * Return available disk space
	 * This will only be called if FILE_STORAGE_QUOTA constant is null
	 */
	static public function getQuota(): int;

	/**
	 * This is called periodically to sync between stored metadata and file storage

	 */
	static public function sync(): void;

	/**
	 * Delete all stored content in this backend
	 */
	static public function reset(): void;

	/**

Modified src/include/lib/Garradin/Web/Skeleton.php from [5ac6b7bd7d] to [58a08193e6].

21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

	public function __construct(string $tpl)
	{
		if (!preg_match('!^[\w\d_-]+(?:\.[\w\d_-]+)*$!i', $tpl)) {
			throw new \InvalidArgumentException('Invalid skeleton name');
		}

		$this->file = Files::getWithNameAndContext($tpl, File::CONTEXT_SKELETON);

		$this->name = $tpl;
	}

	public function defaultPath(): ?string
	{
		$path = ROOT . '/www/skel-dist/' . $this->name;







|







21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

	public function __construct(string $tpl)
	{
		if (!preg_match('!^[\w\d_-]+(?:\.[\w\d_-]+)*$!i', $tpl)) {
			throw new \InvalidArgumentException('Invalid skeleton name');
		}

		$this->file = Files::getFromContext(File::CONTEXT_SKELETON, null, $tpl);

		$this->name = $tpl;
	}

	public function defaultPath(): ?string
	{
		$path = ROOT . '/www/skel-dist/' . $this->name;
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
			$f = new self($file);
			$f->reset();
		}
	}

	static public function list(): array
	{
		$defaults = [];

		$dir = dir(ROOT . '/www/skel-dist/');

		while ($file = $dir->read())
		{
			if ($file[0] == '.')
				continue;

			$defaults[$file] = null;
		}

		$dir->close();


		$modified_skeletons = EM::getInstance(File::class)->DB()->getGrouped('SELECT name, id, modified FROM files WHERE context = ? ORDER BY name;', File::CONTEXT_SKELETON);





		$sources = array_merge($defaults, $modified_skeletons);



		ksort($sources);

		return $sources;
	}

	static public function upload(string $name, ?string $file): void
	{







|








|




>
|
>
>
>
>
|
|
>
>
>







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
			$f = new self($file);
			$f->reset();
		}
	}

	static public function list(): array
	{
		$sources = [];

		$dir = dir(ROOT . '/www/skel-dist/');

		while ($file = $dir->read())
		{
			if ($file[0] == '.')
				continue;

			$sources[$file] = null;
		}

		$dir->close();

		$list = Files::list(File::CONTEXT_SKELETON);

		foreach ($list as $file) {
			if (!is_object($file)) {
				// Ignore directories / FIXME: support directories
				continue;
			}

			$sources[$file->name] = $file;
		}

		ksort($sources);

		return $sources;
	}

	static public function upload(string $name, ?string $file): void
	{