1<?php
2
3namespace Rubix\ML\Persisters;
4
5use Rubix\ML\Encoding;
6use Rubix\ML\Persistable;
7use Rubix\ML\Other\Helpers\Params;
8use Rubix\ML\Persisters\Serializers\Native;
9use Rubix\ML\Persisters\Serializers\Serializer;
10use Rubix\ML\Exceptions\RuntimeException;
11use League\Flysystem\Filesystem;
12use League\Flysystem\FilesystemOperator;
13use League\Flysystem\FilesystemException;
14
15/**
16 * Flysystem
17 *
18 * Flysystem is a filesystem library providing a unified storage interface and abstraction layer.
19 * It enables access to many different storage backends such as Local, Amazon S3, FTP, and more.
20 *
21 * > **Note:** The Flysystem persister is designed to work with Flysystem version 2.0.
22 *
23 * @see https://flysystem.thephpleague.com
24 *
25 * @category    Machine Learning
26 * @package     Rubix/ML
27 * @author      Chris Simpson
28 */
29class Flysystem implements Persister
30{
31    /**
32     * The extension to give files created as part of a persistable's save history.
33     *
34     * @var string
35     */
36    public const HISTORY_EXT = 'old';
37
38    /**
39     * The path to the model file on the filesystem.
40     *
41     * @var string
42     */
43    protected $path;
44
45    /**
46     * The filesystem implementation providing access to your backend storage.
47     *
48     * @var \League\Flysystem\FilesystemOperator
49     */
50    protected $filesystem;
51
52    /**
53     * Should we keep a history of past saves?
54     *
55     * @var bool
56     */
57    protected $history;
58
59    /**
60     * The serializer used to convert to and from serial format.
61     *
62     * @var \Rubix\ML\Persisters\Serializers\Serializer
63     */
64    protected $serializer;
65
66    /**
67     * @param string $path
68     * @param \League\Flysystem\FilesystemOperator $filesystem
69     * @param bool $history
70     * @param \Rubix\ML\Persisters\Serializers\Serializer|null $serializer
71     */
72    public function __construct(
73        string $path,
74        FilesystemOperator $filesystem,
75        bool $history = false,
76        ?Serializer $serializer = null
77    ) {
78        $this->path = $path;
79        $this->filesystem = $filesystem;
80        $this->history = $history;
81        $this->serializer = $serializer ?? new Native();
82    }
83
84    /**
85     * Save the persistable object.
86     *
87     * @param \Rubix\ML\Persistable $persistable
88     * @throws \RuntimeException
89     */
90    public function save(Persistable $persistable) : void
91    {
92        if ($this->history and $this->filesystem->fileExists($this->path)) {
93            $timestamp = time();
94
95            $filename = "{$this->path}-$timestamp." . self::HISTORY_EXT;
96
97            $num = 0;
98
99            while ($this->filesystem->fileExists($filename)) {
100                $filename = "{$this->path}-$timestamp-" . ++$num . '.' . self::HISTORY_EXT;
101            }
102
103            try {
104                $this->filesystem->move($this->path, $filename);
105            } catch (FilesystemException $e) {
106                throw new RuntimeException("Failed to create history file '$filename'.");
107            }
108        }
109
110        $encoding = $this->serializer->serialize($persistable);
111
112        try {
113            $this->filesystem->write($this->path, $encoding);
114        } catch (FilesystemException $e) {
115            throw new RuntimeException('Could not write to filesystem.');
116        }
117    }
118
119    /**
120     * Load the last model that was saved.
121     *
122     * @throws \RuntimeException
123     * @return \Rubix\ML\Persistable
124     */
125    public function load() : Persistable
126    {
127        if (!$this->filesystem->fileExists($this->path)) {
128            throw new RuntimeException("File does not exist at {$this->path}.");
129        }
130
131        try {
132            $data = $this->filesystem->read($this->path);
133        } catch (FilesystemException $e) {
134            throw new RuntimeException("Error reading data from {$this->path}.");
135        }
136
137        $encoding = new Encoding($data);
138
139        if ($encoding->bytes() === 0) {
140            throw new RuntimeException("File at {$this->path} does not contain any data.");
141        }
142
143        return $this->serializer->unserialize($encoding);
144    }
145
146    /**
147     * Return the string representation of the object.
148     *
149     * @return string
150     */
151    public function __toString() : string
152    {
153        return "Flysystem (path: {$this->path}, filesystem: "
154            . Params::toString($this->filesystem) . ', history: '
155            . Params::toString($this->history) . ', serializer: '
156            . "{$this->serializer})";
157    }
158}
159