Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
9 / 9
CRAP
100.00% covered (success)
100.00%
1 / 1
Storage
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
9 / 9
11
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 key
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 write
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 read
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 delete
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 deleteDirectory
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 exists
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 move
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 path
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray\Storage;
6
7use Cosray\Assets\Util;
8use Cosray\Config;
9use Cosray\Util\Path;
10use League\Flysystem\Filesystem;
11use League\Flysystem\Local\LocalFilesystemAdapter;
12
13/**
14 * Storage seam for the asset pool.
15 *
16 * Keys are sharded per-asset paths like `ab/abcdefghij123/logo.png`,
17 * relative to the disk root. The only disk in phase 1a is `local`, rooted
18 * at the public assets directory. path() resolves a key to an absolute
19 * local file, which the resize pipeline, Response::file() and X-Sendfile
20 * require; a non-local disk will need a temp-download strategy for those
21 * before it can exist.
22 */
23class Storage
24{
25    public readonly string $disk;
26    protected readonly Filesystem $filesystem;
27    protected readonly string $root;
28
29    public function __construct(Config $config)
30    {
31        $this->disk = 'local';
32        $this->root = rtrim($config->path->public, '\\/') . '/' . trim($config->path->assets, '/');
33        $this->filesystem = new Filesystem(new LocalFilesystemAdapter($this->root));
34    }
35
36    /**
37     * The pool key for an asset: `{uid[:2]}/{uid}/{slug}`. One directory
38     * per asset, so slugs can never collide. Names whose slug loses its
39     * stem (e.g. all-CJK filenames) fall back to the uid as stem.
40     */
41    public static function key(string $uid, string $filename): string
42    {
43        $slug = Util::slug($filename);
44
45        if ($slug === '' || str_starts_with($slug, '.')) {
46            $slug = $uid . $slug;
47        }
48
49        return substr($uid, 0, 2) . "/{$uid}/{$slug}";
50    }
51
52    public function write(string $key, string $contents): void
53    {
54        $this->filesystem->write($key, $contents);
55    }
56
57    public function read(string $key): string
58    {
59        return $this->filesystem->read($key);
60    }
61
62    public function delete(string $key): void
63    {
64        $this->filesystem->delete($key);
65    }
66
67    /** Removes an asset's whole pool directory (`{shard}/{uid}/`). */
68    public function deleteDirectory(string $path): void
69    {
70        $this->filesystem->deleteDirectory($path);
71    }
72
73    public function exists(string $key): bool
74    {
75        return $this->filesystem->fileExists($key);
76    }
77
78    public function move(string $source, string $target): void
79    {
80        $this->filesystem->move($source, $target);
81    }
82
83    /** Absolute local path for an existing key; only valid on the local disk. */
84    public function path(string $key): string
85    {
86        return Path::inside($this->root, $key, checkIsFile: true);
87    }
88}