├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml ├── src ├── Client.php ├── GitlabAdapter.php └── UnableToRetrieveFileTree.php └── tests ├── ClientTest.php ├── GitlabAdapterTest.php ├── TestCase.php ├── assets ├── testing-update.txt └── testing.txt └── config └── config.testing.example.php /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | vendor 3 | .DS_Store 4 | .idea 5 | .phpunit.result.cache 6 | tests/config/config.testing.php -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Changelog 2 | 3 | All notable changes to `flysystem-gitlab-storage` will be documented in this file 4 | 5 | ### 0.0.1 - 2019-08-02 6 | - initial commit. 7 | 8 | ### 1.0.0 - 2019-08-03 9 | - Implemented createDir by creating a directory with a .gitkeep file. 10 | - Small bug fixes. 11 | 12 | ### 1.0.1 - 2019-08-03 13 | - Updated CHANGELOG. 14 | 15 | ### 1.0.2 - 2019-08-03 16 | - Updated composer.json to support laravel projects. 17 | 18 | ### 1.0.3 - 2019-08-03 19 | - Fixed packagist versioning issue. 20 | 21 | ### 1.0.4 - 2019-08-03 22 | - Added support for tree path with multiple sub folders. 23 | 24 | ### 1.0.5 - 2019-08-03 25 | - Adapters read method now returns an array instead of raw content. 26 | 27 | ### 1.0.6 - 2019-08-03 28 | - Adapters listContents method now changes type blob to type file. 29 | 30 | ### 1.0.7 - 2020-03-20 31 | - Added a debug mode. 32 | 33 | ### 1.1.0 - 2020-06-29 34 | - Moved minimum PHP version to 7.1 since PHPUnit 9 requires 7.1 or above. 35 | - Added support for paginated list of contents when requesting file trees. 36 | - [https://docs.gitlab.com/ee/api/README.html#pagination](https://docs.gitlab.com/ee/api/README.html#pagination) 37 | 38 | ### 2.0.0 - 2020-11-30 39 | - Migrated to flysystem 2.x 40 | 41 | ### 2.0.1 - 2020-12-01 42 | - Added php 8 support 43 | 44 | ### 2.0.2 - 2020-12-01 45 | - Allow to read into stream 46 | 47 | ### 2.0.3 - 2020-12-16 48 | - Reuse stream of HTTP request instead of create new stream 49 | 50 | ### 2.0.4 - 2020-12-16 51 | - Savings HTTP exchanges with HEAD request 52 | 53 | ### 3.0.0 - 2022-01-27 54 | - Added flysystem v3 support. 55 | 56 | ### 3.1.0 - 2024-04-26 57 | - Moved minimum PHP version to 8.1 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Roy Voetman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | A Gitlab Storage filesystem for [Flysystem](https://flysystem.thephpleague.com/docs/). 4 | 5 | [![Latest Version](https://img.shields.io/packagist/v/royvoetman/flysystem-gitlab-storage.svg?style=flat-square)](https://packagist.org/packages/royvoetman/flysystem-gitlab-storage) 6 | [![MIT Licensed](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) 7 | [![Total Downloads](https://img.shields.io/packagist/dt/royvoetman/flysystem-gitlab-storage.svg?style=flat-square)](https://packagist.org/packages/royvoetman/flysystem-gitlab-storage) 8 | 9 | This package contains a Flysystem adapter for Gitlab. Under the hood, Gitlab's [Repository (files) API](https://docs.gitlab.com/ee/api/repository_files.html) v4 is used. 10 | 11 | > For Flysystem 2 (PHP 7.4) [use version 2.0.4](https://github.com/RoyVoetman/flysystem-gitlab-storage/tree/v2.0.4) 12 | 13 | > For Flysystem 1 (PHP 7.1) [use version 1.1.0](https://github.com/RoyVoetman/flysystem-gitlab-storage/tree/v1.1.0) 14 | 15 | ## Installation 16 | 17 | ```bash 18 | composer require royvoetman/flysystem-gitlab-storage 19 | ``` 20 | 21 | ## Integrations 22 | 23 | * Laravel - [https://github.com/royvoetman/laravel-gitlab-storage](https://github.com/royvoetman/laravel-gitlab-storage) 24 | 25 | ## Usage 26 | ```php 27 | // Create a Gitlab Client to talk with the API 28 | $client = new Client('project-id', 'branch', 'base-url', 'personal-access-token'); 29 | 30 | // Create the Adapter that implements Flysystems AdapterInterface 31 | $adapter = new GitlabAdapter( 32 | // Gitlab API Client 33 | $client, 34 | // Optional path prefix 35 | 'path/prefix', 36 | ); 37 | 38 | // The FilesystemOperator 39 | $filesystem = new League\Flysystem\Filesystem($adapter); 40 | 41 | // see http://flysystem.thephpleague.com/api/ for full list of available functionality 42 | ``` 43 | 44 | ### Project ID 45 | Every project in Gitlab has its own Project ID. It can be found at to top of the frontpage of your repository. [See](https://stackoverflow.com/questions/39559689/where-do-i-find-the-project-id-for-the-gitlab-api#answer-53126068) 46 | 47 | ### Base URL 48 | This will be the URL where you host your gitlab server (e.g. https://gitlab.com) 49 | 50 | ### Access token (required for private projects) 51 | Gitlab supports server side API authentication with Personal Access tokens 52 | 53 | For more information on how to create your own Personal Access token: [Gitlab Docs](https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html) 54 | 55 | ## Changelog 56 | 57 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. 58 | 59 | ## Contributing 60 | 61 | Contributions are **welcome** and will be fully **credited**. We accept contributions via Pull Requests on [Github](https://github.com/RoyVoetman/flysystem-gitlab-storage). 62 | 63 | ### Pull Requests 64 | 65 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer). 66 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 67 | - **Create feature branches** - Don't ask us to pull from your master branch. 68 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 69 | 70 | ## License 71 | 72 | The MIT License (MIT). Please see [License File](LICENSE) for more information. 73 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "royvoetman/flysystem-gitlab-storage", 3 | "description": "Flysystem Adapter for the Gitlab Repository files API v4", 4 | "keywords": [ 5 | "royvoetman", 6 | "flysystem-gitlab", 7 | "flysystem", 8 | "gitlab", 9 | "v4", 10 | "api", 11 | "laravel-storage" 12 | ], 13 | "type": "library", 14 | "license": "MIT", 15 | "authors": [ 16 | { 17 | "name": "Roy Voetman", 18 | "email": "royvoetman@outlook.com", 19 | "role": "Developer" 20 | } 21 | ], 22 | "require": { 23 | "php": "^8.1", 24 | "ext-json": "*", 25 | "guzzlehttp/guzzle": "^7.0", 26 | "league/flysystem": "^2.0 || ^3.0" 27 | }, 28 | "require-dev": { 29 | "phpunit/phpunit": "^10.5.11 || ^11.0.4" 30 | }, 31 | "autoload": { 32 | "psr-4": { 33 | "RoyVoetman\\FlysystemGitlab\\": "src/" 34 | } 35 | }, 36 | "autoload-dev": { 37 | "psr-4": { 38 | "RoyVoetman\\FlysystemGitlab\\Tests\\": "tests" 39 | } 40 | }, 41 | "config": { 42 | "sort-packages": true 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | ./tests 10 | 11 | 12 | 13 | 14 | 15 | ./src 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Client.php: -------------------------------------------------------------------------------- 1 | projectId = $projectId; 50 | $this->branch = $branch; 51 | $this->baseUrl = $baseUrl; 52 | $this->personalAccessToken = $personalAccessToken; 53 | } 54 | 55 | /** 56 | * @param $path 57 | * 58 | * @return string 59 | * @throws \GuzzleHttp\Exception\GuzzleException 60 | */ 61 | public function readRaw(string $path): string 62 | { 63 | $path = rawurlencode($path); 64 | 65 | $response = $this->request('GET', "files/$path/raw"); 66 | 67 | return $this->responseContents($response, false); 68 | } 69 | 70 | /** 71 | * @param $path 72 | * 73 | * @return array 74 | * @throws \GuzzleHttp\Exception\GuzzleException 75 | */ 76 | public function read($path) 77 | { 78 | $path = rawurlencode($path); 79 | 80 | $response = $this->request('HEAD', "files/$path"); 81 | 82 | $headers = $response->getHeaders(); 83 | $headers = array_filter( 84 | $headers, 85 | fn($key) => substr($key, 0, 9) == 'x-gitlab-', 86 | ARRAY_FILTER_USE_KEY 87 | ); 88 | 89 | $keys = array_keys($headers); 90 | $values = array_values($headers); 91 | 92 | array_walk( 93 | $keys, 94 | function(&$key) { 95 | $key = substr($key, 9); 96 | $key = strtolower($key); 97 | $key = preg_replace_callback( 98 | '/[-_]+(.)?/i', 99 | fn($matches) => strtoupper($matches[ 1 ]), 100 | $key 101 | ); 102 | } 103 | ); 104 | 105 | return array_combine($keys, $values); 106 | } 107 | 108 | /** 109 | * @param $path 110 | * 111 | * @return resource|null 112 | * @throws \GuzzleHttp\Exception\GuzzleException 113 | */ 114 | public function readStream($path) 115 | { 116 | $path = rawurlencode($path); 117 | 118 | $response = $this->request('GET', "files/$path/raw"); 119 | 120 | return $response->getBody()->detach(); 121 | } 122 | 123 | /** 124 | * @param $path 125 | * 126 | * @return mixed|string 127 | * @throws \GuzzleHttp\Exception\GuzzleException 128 | */ 129 | public function blame($path) 130 | { 131 | $path = rawurlencode($path); 132 | 133 | $response = $this->request('GET', "files/$path/blame"); 134 | 135 | return $this->responseContents($response); 136 | } 137 | 138 | /** 139 | * @param string $path 140 | * @param string $contents 141 | * @param string $commitMessage 142 | * @param bool $override 143 | * 144 | * @return array 145 | * @throws \GuzzleHttp\Exception\GuzzleException 146 | */ 147 | public function upload(string $path, string $contents, string $commitMessage, $override = false): array 148 | { 149 | $path = rawurlencode($path); 150 | 151 | $method = $override ? 'PUT' : 'POST'; 152 | 153 | $response = $this->request($method, "files/$path", [ 154 | 'content' => $contents, 155 | 'commit_message' => $commitMessage 156 | ]); 157 | 158 | return $this->responseContents($response); 159 | } 160 | 161 | /** 162 | * @param string $path 163 | * @param $resource 164 | * @param string $commitMessage 165 | * @param bool $override 166 | * 167 | * @return array 168 | * @throws \GuzzleHttp\Exception\GuzzleException 169 | */ 170 | public function uploadStream(string $path, $resource, string $commitMessage, $override = false): array 171 | { 172 | if (!is_resource($resource)) { 173 | throw new InvalidArgumentException(sprintf('Argument must be a valid resource type. %s given.', 174 | gettype($resource))); 175 | } 176 | 177 | return $this->upload($path, stream_get_contents($resource), $commitMessage, $override); 178 | } 179 | 180 | /** 181 | * @param string $path 182 | * @param string $commitMessage 183 | * 184 | * @throws \GuzzleHttp\Exception\GuzzleException 185 | */ 186 | public function delete(string $path, string $commitMessage) 187 | { 188 | $path = rawurlencode($path); 189 | 190 | $this->request('DELETE', "files/$path", [ 191 | 'commit_message' => $commitMessage 192 | ]); 193 | } 194 | 195 | /** 196 | * @param string|null $directory 197 | * @param bool $recursive 198 | * 199 | * @return iterable 200 | * @throws \GuzzleHttp\Exception\GuzzleException 201 | */ 202 | public function tree(string $directory = null, bool $recursive = false): iterable 203 | { 204 | if ($directory === '/' || $directory === '') { 205 | $directory = null; 206 | } 207 | 208 | $page = 1; 209 | 210 | do { 211 | $response = $this->request('GET', 'tree', [ 212 | 'path' => $directory, 213 | 'recursive' => $recursive, 214 | 'per_page' => 100, 215 | 'page' => $page++ 216 | ]); 217 | 218 | yield $this->responseContents($response); 219 | } while ($this->responseHasNextPage($response)); 220 | } 221 | 222 | /** 223 | * @return string 224 | */ 225 | public function getPersonalAccessToken(): string 226 | { 227 | return $this->personalAccessToken; 228 | } 229 | 230 | /** 231 | * @param string $personalAccessToken 232 | */ 233 | public function setPersonalAccessToken(string $personalAccessToken) 234 | { 235 | $this->personalAccessToken = $personalAccessToken; 236 | } 237 | 238 | /** 239 | * @return string 240 | */ 241 | public function getProjectId(): string 242 | { 243 | return $this->projectId; 244 | } 245 | 246 | /** 247 | * @param string $projectId 248 | */ 249 | public function setProjectId(string $projectId) 250 | { 251 | $this->projectId = $projectId; 252 | } 253 | 254 | /** 255 | * @return string 256 | */ 257 | public function getBranch(): string 258 | { 259 | return $this->branch; 260 | } 261 | 262 | /** 263 | * @param string $branch 264 | */ 265 | public function setBranch(string $branch) 266 | { 267 | $this->branch = $branch; 268 | } 269 | 270 | /** 271 | * @param string $method 272 | * @param string $uri 273 | * @param array $params 274 | * 275 | * @return \GuzzleHttp\Psr7\Response 276 | * @throws \GuzzleHttp\Exception\GuzzleException 277 | */ 278 | private function request(string $method, string $uri, array $params = []): Response 279 | { 280 | $uri = !in_array($method, ['POST', 'PUT', 'DELETE']) ? $this->buildUri($uri, $params) : $this->buildUri($uri); 281 | $params = in_array($method, ['POST', 'PUT', 'DELETE']) ? ['form_params' => array_merge(['branch' => $this->branch], $params)] : []; 282 | 283 | $client = new HttpClient(['headers' => ['PRIVATE-TOKEN' => $this->personalAccessToken]]); 284 | 285 | return $client->request($method, $uri, $params); 286 | } 287 | 288 | /** 289 | * @param string $uri 290 | * @param $params 291 | * 292 | * @return string 293 | */ 294 | private function buildUri(string $uri, array $params = []): string 295 | { 296 | $params = array_merge(['ref' => $this->branch], $params); 297 | 298 | $params = array_map(function($value) { 299 | return $value !== null ? rawurlencode($value) : null; 300 | }, $params); 301 | 302 | if(isset($params['path'])) { 303 | $params['path'] = urldecode($params['path']); 304 | } 305 | 306 | $params = http_build_query($params); 307 | 308 | $params = !empty($params) ? "?$params" : null; 309 | 310 | $baseUrl = rtrim($this->baseUrl, '/').self::VERSION_URI; 311 | 312 | return "{$baseUrl}/projects/{$this->projectId}/repository/{$uri}{$params}"; 313 | } 314 | 315 | /** 316 | * @param \GuzzleHttp\Psr7\Response $response 317 | * @param bool $json 318 | * 319 | * @return mixed|string 320 | */ 321 | private function responseContents(Response $response, $json = true) 322 | { 323 | $contents = $response->getBody() 324 | ->getContents(); 325 | 326 | return ($json) ? json_decode($contents, true) : $contents; 327 | } 328 | 329 | /** 330 | * @param \GuzzleHttp\Psr7\Response $response 331 | * 332 | * @return bool 333 | */ 334 | private function responseHasNextPage(Response $response) 335 | { 336 | if ($response->hasHeader('X-Next-Page')) { 337 | return !empty($response->getHeader('X-Next-Page')[0] ?? ""); 338 | } 339 | 340 | return false; 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /src/GitlabAdapter.php: -------------------------------------------------------------------------------- 1 | client = $client; 47 | $this->prefixer = new PathPrefixer($prefix, DIRECTORY_SEPARATOR); 48 | $this->mimeTypeDetector = new ExtensionMimeTypeDetector(); 49 | } 50 | 51 | /** 52 | * @inheritdoc 53 | */ 54 | public function fileExists(string $path): bool 55 | { 56 | try { 57 | $this->client->read($this->prefixer->prefixPath($path)); 58 | } catch (Throwable $e) { 59 | if ($e instanceof ClientException && $e->getCode() == 404) { 60 | return false; 61 | } 62 | 63 | throw UnableToCheckFileExistence::forLocation($path, $e); 64 | } 65 | 66 | return true; 67 | } 68 | 69 | /** 70 | * @inheritdoc 71 | */ 72 | public function write(string $path, string $contents, Config $config): void 73 | { 74 | $location = $this->prefixer->prefixPath($path); 75 | 76 | try { 77 | $override = $this->fileExists($location); 78 | 79 | $this->client->upload($location, $contents, self::UPLOADED_FILE_COMMIT_MESSAGE, $override); 80 | } catch (Throwable $e) { 81 | throw UnableToWriteFile::atLocation($path, $e->getMessage(), $e); 82 | } 83 | } 84 | 85 | /** 86 | * @inheritdoc 87 | */ 88 | public function writeStream(string $path, $contents, Config $config): void 89 | { 90 | $location = $this->prefixer->prefixPath($path); 91 | 92 | try { 93 | $override = $this->fileExists($location); 94 | 95 | $this->client->uploadStream($location, $contents, self::UPLOADED_FILE_COMMIT_MESSAGE, $override); 96 | } catch (Throwable $e) { 97 | throw UnableToWriteFile::atLocation($path, $e->getMessage(), $e); 98 | } 99 | } 100 | 101 | /** 102 | * @inheritdoc 103 | */ 104 | public function read(string $path): string 105 | { 106 | try { 107 | return $this->client->readRaw($this->prefixer->prefixPath($path)); 108 | } catch (Throwable $e) { 109 | throw UnableToReadFile::fromLocation($path, $e->getMessage(), $e); 110 | } 111 | } 112 | 113 | /** 114 | * @inheritdoc 115 | */ 116 | public function readStream(string $path) 117 | { 118 | try { 119 | if (null === ($resource = $this->client->readStream($this->prefixer->prefixPath($path)))) { 120 | throw UnableToReadFile::fromLocation($path, 'Empty content'); 121 | } 122 | 123 | return $resource; 124 | } catch (Throwable $e) { 125 | throw UnableToReadFile::fromLocation($path, $e->getMessage(), $e); 126 | } 127 | } 128 | 129 | /** 130 | * @inheritdoc 131 | */ 132 | public function delete(string $path): void 133 | { 134 | try { 135 | $this->client->delete($this->prefixer->prefixPath($path), self::DELETED_FILE_COMMIT_MESSAGE); 136 | } catch (Throwable $e) { 137 | throw UnableToDeleteFile::atLocation($path, $e->getMessage(), $e); 138 | } 139 | } 140 | 141 | /** 142 | * @inheritdoc 143 | */ 144 | public function deleteDirectory(string $path): void 145 | { 146 | $files = $this->listContents($this->prefixer->prefixPath($path), false); 147 | 148 | /** @var StorageAttributes $file */ 149 | foreach ($files as $file) { 150 | if ($file->isFile()) { 151 | try { 152 | $this->client->delete($file->path(), self::DELETED_FILE_COMMIT_MESSAGE); 153 | } catch (Throwable $e) { 154 | throw UnableToDeleteDirectory::atLocation($path, $e->getMessage(), $e); 155 | } 156 | } 157 | } 158 | } 159 | 160 | /** 161 | * @inheritdoc 162 | */ 163 | public function createDirectory(string $path, Config $config): void 164 | { 165 | $path = rtrim($path, '/') . '/.gitkeep'; 166 | 167 | try { 168 | $this->write($this->prefixer->prefixPath($path), '', $config); 169 | } catch (Throwable $e) { 170 | throw UnableToCreateDirectory::dueToFailure($path, $e); 171 | } 172 | } 173 | 174 | /** 175 | * @inheritdoc 176 | * 177 | * @throws \League\Flysystem\UnableToSetVisibility 178 | */ 179 | public function setVisibility(string $path, $visibility): void 180 | { 181 | throw new UnableToSetVisibility(get_class($this).' Gitlab API does not support visibility.'); 182 | } 183 | 184 | /** 185 | * @inheritdoc 186 | * 187 | * @throws \League\Flysystem\UnableToSetVisibility 188 | */ 189 | public function visibility(string $path): FileAttributes 190 | { 191 | throw new UnableToSetVisibility(get_class($this).' Gitlab API does not support visibility.'); 192 | } 193 | 194 | /** 195 | * @inheritdoc 196 | * 197 | * @throws \League\Flysystem\UnableToRetrieveMetadata 198 | */ 199 | public function mimeType(string $path): FileAttributes 200 | { 201 | $mimeType = $this->mimeTypeDetector->detectMimeTypeFromPath($this->prefixer->prefixPath($path)); 202 | 203 | if ($mimeType === null) { 204 | throw UnableToRetrieveMetadata::mimeType($path); 205 | } 206 | 207 | return new FileAttributes($path, null, null, null, $mimeType); 208 | } 209 | 210 | /** 211 | * @inheritdoc 212 | */ 213 | public function lastModified(string $path): FileAttributes 214 | { 215 | try { 216 | $response = $this->client->blame($this->prefixer->prefixPath($path)); 217 | 218 | if (empty($response)) { 219 | return new FileAttributes($path, null, null, null); 220 | } 221 | 222 | $lastModified = DateTime::createFromFormat("Y-m-d\TH:i:s.uO", $response[0]['commit']['committed_date']); 223 | 224 | return new FileAttributes($path, null, null, $lastModified->getTimestamp()); 225 | } catch (Throwable $e) { 226 | throw UnableToRetrieveMetadata::lastModified($path, $e->getMessage(), $e); 227 | } 228 | } 229 | 230 | /** 231 | * @inheritdoc 232 | */ 233 | public function fileSize(string $path): FileAttributes 234 | { 235 | try { 236 | $meta = $this->client->read($this->prefixer->prefixPath($path)); 237 | 238 | return new FileAttributes($path, $meta['size'][0] ?? 0); 239 | } catch (Throwable $e) { 240 | throw UnableToRetrieveMetadata::fileSize($path, $e->getMessage(), $e); 241 | } 242 | } 243 | 244 | /** 245 | * @inheritdoc 246 | */ 247 | public function listContents(string $path, bool $deep): iterable 248 | { 249 | try { 250 | $tree = $this->client->tree($this->prefixer->prefixPath($path), $deep); 251 | 252 | foreach ($tree as $folders) { 253 | foreach ($folders as $item) { 254 | $isDirectory = $item['type'] == 'tree'; 255 | 256 | yield $isDirectory ? new DirectoryAttributes($item['path'], null, null) : new FileAttributes( 257 | $item['path'], 258 | $this->fileSize($item['path'])->fileSize(), 259 | null, 260 | $this->lastModified($item['path'])->lastModified(), 261 | $this->mimeTypeDetector->detectMimeTypeFromPath($item['path']) 262 | ); 263 | } 264 | } 265 | } catch (Throwable $e) { 266 | throw new UnableToRetrieveFileTree($e->getMessage()); 267 | } 268 | } 269 | 270 | /** 271 | * @inheritdoc 272 | */ 273 | public function move(string $source, string $destination, Config $config): void 274 | { 275 | try { 276 | $contents = $this->client->readRaw($this->prefixer->prefixPath($source)); 277 | 278 | $this->client->upload( 279 | $this->prefixer->prefixPath($destination), 280 | $contents, 281 | self::UPLOADED_FILE_COMMIT_MESSAGE 282 | ); 283 | 284 | $this->client->delete($this->prefixer->prefixPath($source), self::DELETED_FILE_COMMIT_MESSAGE); 285 | } catch (Throwable $e) { 286 | throw UnableToMoveFile::fromLocationTo($source, $destination, $e); 287 | } 288 | } 289 | 290 | /** 291 | * @inheritdoc 292 | */ 293 | public function copy(string $source, string $destination, Config $config): void 294 | { 295 | try { 296 | $contents = $this->client->readRaw($this->prefixer->prefixPath($source)); 297 | 298 | $this->client->upload( 299 | $this->prefixer->prefixPath($destination), 300 | $contents, 301 | self::UPLOADED_FILE_COMMIT_MESSAGE 302 | ); 303 | } catch (Throwable $e) { 304 | throw UnableToCopyFile::fromLocationTo($source, $destination, $e); 305 | } 306 | } 307 | 308 | public function getClient(): Client 309 | { 310 | return $this->client; 311 | } 312 | 313 | public function setClient(Client $client) 314 | { 315 | $this->client = $client; 316 | } 317 | 318 | /** 319 | * @inheritdoc 320 | */ 321 | public function directoryExists(string $path): bool 322 | { 323 | try { 324 | $tree = $this->client->tree($this->prefixer->prefixPath($path)); 325 | 326 | return (bool)count($tree->current()); 327 | } catch (Throwable $e) { 328 | if ($e instanceof ClientException && $e->getCode() == 404) { 329 | return false; 330 | } 331 | 332 | throw UnableToCheckExistence::forLocation($path, $e); 333 | } 334 | } 335 | } 336 | -------------------------------------------------------------------------------- /src/UnableToRetrieveFileTree.php: -------------------------------------------------------------------------------- 1 | client = $this->getClientInstance(); 18 | } 19 | 20 | #[Test] 21 | public function it_can_be_instantiated() 22 | { 23 | $this->assertInstanceOf(Client::class, $this->getClientInstance()); 24 | } 25 | 26 | #[Test] 27 | public function it_can_read_a_file() 28 | { 29 | $meta = $this->client->read('README.md'); 30 | 31 | $this->assertArrayHasKey('ref', $meta); 32 | $this->assertArrayHasKey('size', $meta); 33 | $this->assertArrayHasKey('lastCommitId', $meta); 34 | } 35 | 36 | #[Test] 37 | public function it_can_read_a_file_raw() 38 | { 39 | $content = $this->client->readRaw('README.md'); 40 | 41 | $this->assertStringStartsWith('# Testing repo for `flysystem-gitlab`', $content); 42 | } 43 | 44 | #[Test] 45 | public function it_can_create_a_file() 46 | { 47 | $contents = $this->client->upload('testing.md', '# Testing create', 'Created file'); 48 | 49 | $this->assertStringStartsWith('# Testing create', $this->client->readRaw('testing.md')); 50 | $this->assertSame($contents, [ 51 | 'file_path' => 'testing.md', 52 | 'branch' => $this->client->getBranch() 53 | ]); 54 | } 55 | 56 | #[Test] 57 | public function it_can_update_a_file() 58 | { 59 | $contents = $this->client->upload('testing.md', '# Testing update', 'Updated file', true); 60 | 61 | $this->assertStringStartsWith('# Testing update', $this->client->readRaw('testing.md')); 62 | $this->assertSame($contents, [ 63 | 'file_path' => 'testing.md', 64 | 'branch' => $this->client->getBranch() 65 | ]); 66 | } 67 | 68 | #[Test] 69 | public function it_can_delete_a_file() 70 | { 71 | $this->client->delete('testing.md', 'Deleted file'); 72 | 73 | $this->expectException(ClientException::class); 74 | 75 | $this->client->read('testing.md'); 76 | } 77 | 78 | #[Test] 79 | public function it_can_create_a_file_from_stream() 80 | { 81 | $stream = fopen(__DIR__.'/assets/testing.txt', 'r+'); 82 | 83 | $contents = $this->client->uploadStream('testing.txt', $stream, 'Created file'); 84 | 85 | fclose($stream); 86 | 87 | $this->assertStringStartsWith('File for testing file streams', $this->client->readRaw('testing.txt')); 88 | $this->assertSame($contents, [ 89 | 'file_path' => 'testing.txt', 90 | 'branch' => $this->client->getBranch() 91 | ]); 92 | 93 | // Clean up 94 | $this->client->delete('testing.txt', 'Deleted file'); 95 | } 96 | 97 | #[Test] 98 | public function it_can_not_a_create_file_from_stream_without_a_valid_stream() 99 | { 100 | $this->expectException(\InvalidArgumentException::class); 101 | 102 | $this->client->uploadStream('testing.txt', 'string of data', 'Created file'); 103 | } 104 | 105 | #[Test] 106 | public function it_can_retrieve_a_file_tree() 107 | { 108 | $contents = $this->client->tree(); 109 | 110 | $content = $contents->current(); 111 | 112 | $this->assertIsArray($content); 113 | $this->assertArrayHasKey('id', $content[0]); 114 | $this->assertArrayHasKey('name', $content[0]); 115 | $this->assertArrayHasKey('type', $content[0]); 116 | $this->assertArrayHasKey('path', $content[0]); 117 | $this->assertArrayHasKey('mode', $content[0]); 118 | } 119 | 120 | #[Test] 121 | public function it_can_retrieve_a_file_tree_recursive() 122 | { 123 | $contents = $this->client->tree('/', true); 124 | 125 | $content = $contents->current(); 126 | 127 | $this->assertIsArray($content); 128 | } 129 | 130 | #[Test] 131 | public function it_can_retrieve_a_file_tree_of_a_subdirectory() 132 | { 133 | $contents = $this->client->tree('recursive', true); 134 | 135 | $content = $contents->current(); 136 | 137 | $this->assertIsArray($content); 138 | $this->assertArrayHasKey('id', $content[0]); 139 | $this->assertArrayHasKey('name', $content[0]); 140 | $this->assertArrayHasKey('type', $content[0]); 141 | $this->assertArrayHasKey('path', $content[0]); 142 | $this->assertArrayHasKey('mode', $content[0]); 143 | } 144 | 145 | #[Test] 146 | public function it_can_change_the_branch() 147 | { 148 | $this->client->setBranch('dev'); 149 | 150 | $this->assertEquals('dev', $this->client->getBranch()); 151 | } 152 | 153 | #[Test] 154 | public function it_can_change_the_project_id() 155 | { 156 | $this->client->setProjectId('12345678'); 157 | 158 | $this->assertEquals('12345678', $this->client->getProjectId()); 159 | } 160 | 161 | #[Test] 162 | public function it_can_change_the_personal_access_token() 163 | { 164 | $this->client->setPersonalAccessToken('12345678'); 165 | 166 | $this->assertEquals('12345678', $this->client->getPersonalAccessToken()); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /tests/GitlabAdapterTest.php: -------------------------------------------------------------------------------- 1 | gitlabAdapter = $this->getAdapterInstance(); 29 | } 30 | 31 | #[Test] 32 | public function it_can_be_instantiated() 33 | { 34 | $this->assertInstanceOf(GitlabAdapter::class, $this->getAdapterInstance()); 35 | } 36 | 37 | #[Test] 38 | public function it_can_retrieve_client_instance() 39 | { 40 | $this->assertInstanceOf(Client::class, $this->gitlabAdapter->getClient()); 41 | } 42 | 43 | #[Test] 44 | public function it_can_set_client_instance() 45 | { 46 | $this->setInvalidProjectId(); 47 | 48 | $this->assertEquals($this->gitlabAdapter->getClient() 49 | ->getProjectId(), '123'); 50 | } 51 | 52 | #[Test] 53 | public function it_can_read_a_file() 54 | { 55 | $response = $this->gitlabAdapter->read('README.md'); 56 | 57 | $this->assertStringStartsWith('# Testing repo for `flysystem-gitlab`', $response); 58 | } 59 | 60 | #[Test] 61 | public function it_can_read_a_file_into_a_stream() 62 | { 63 | $stream = $this->gitlabAdapter->readStream('README.md'); 64 | 65 | $this->assertIsResource($stream); 66 | $this->assertEquals(stream_get_contents($stream, -1, 0), $this->gitlabAdapter->read('README.md')); 67 | } 68 | 69 | #[Test] 70 | public function it_throws_when_read_failed() 71 | { 72 | $this->setInvalidProjectId(); 73 | 74 | $this->expectException(UnableToReadFile::class); 75 | 76 | $this->gitlabAdapter->read('README.md'); 77 | } 78 | 79 | #[Test] 80 | public function it_can_determine_if_a_project_has_a_file() 81 | { 82 | $this->assertTrue($this->gitlabAdapter->fileExists('/README.md')); 83 | 84 | $this->assertFalse($this->gitlabAdapter->fileExists('/I_DONT_EXIST.md')); 85 | } 86 | 87 | #[Test] 88 | public function it_throws_when_file_existence_failed() 89 | { 90 | $this->setInvalidToken(); 91 | 92 | $this->expectException(UnableToCheckFileExistence::class); 93 | 94 | $this->gitlabAdapter->fileExists('/README.md'); 95 | } 96 | 97 | #[Test] 98 | public function it_can_delete_a_file() 99 | { 100 | $this->gitlabAdapter->write('testing.md', '# Testing create', new Config()); 101 | 102 | $this->assertTrue($this->gitlabAdapter->fileExists('/testing.md')); 103 | 104 | $this->gitlabAdapter->delete('/testing.md'); 105 | 106 | $this->assertFalse($this->gitlabAdapter->fileExists('/testing.md')); 107 | } 108 | 109 | #[Test] 110 | public function it_returns_false_when_delete_failed() 111 | { 112 | $this->setInvalidProjectId(); 113 | 114 | $this->expectException(UnableToDeleteFile::class); 115 | 116 | $this->gitlabAdapter->delete('testing_renamed.md'); 117 | } 118 | 119 | #[Test] 120 | public function it_can_write_a_new_file() 121 | { 122 | $this->gitlabAdapter->write('testing.md', '# Testing create', new Config()); 123 | 124 | $this->assertTrue($this->gitlabAdapter->fileExists('testing.md')); 125 | $this->assertEquals('# Testing create', $this->gitlabAdapter->read('testing.md')); 126 | 127 | $this->gitlabAdapter->delete('testing.md'); 128 | } 129 | 130 | #[Test] 131 | public function it_automatically_creates_missing_directories() 132 | { 133 | $this->gitlabAdapter->write('/folder/missing/testing.md', '# Testing create folders', new Config()); 134 | 135 | $this->assertTrue($this->gitlabAdapter->fileExists('/folder/missing/testing.md')); 136 | $this->assertEquals('# Testing create folders', $this->gitlabAdapter->read('/folder/missing/testing.md')); 137 | 138 | $this->gitlabAdapter->delete('/folder/missing/testing.md'); 139 | } 140 | 141 | #[Test] 142 | public function it_throws_when_write_failed() 143 | { 144 | $this->setInvalidProjectId(); 145 | 146 | $this->expectException(UnableToWriteFile::class); 147 | 148 | $this->gitlabAdapter->write('testing.md', '# Testing create', new Config()); 149 | } 150 | 151 | #[Test] 152 | public function it_can_write_a_file_stream() 153 | { 154 | $stream = fopen(__DIR__.'/assets/testing.txt', 'r+'); 155 | $this->gitlabAdapter->writeStream('testing.txt', $stream, new Config()); 156 | fclose($stream); 157 | 158 | $this->assertTrue($this->gitlabAdapter->fileExists('testing.txt')); 159 | $this->assertEquals('File for testing file streams', $this->gitlabAdapter->read('testing.txt')); 160 | 161 | $this->gitlabAdapter->delete('testing.txt'); 162 | } 163 | 164 | #[Test] 165 | public function it_throws_when_writing_file_stream_failed() 166 | { 167 | $this->setInvalidProjectId(); 168 | 169 | $this->expectException(UnableToWriteFile::class); 170 | 171 | $stream = fopen(__DIR__.'/assets/testing.txt', 'r+'); 172 | $this->gitlabAdapter->writeStream('testing.txt', $stream, new Config()); 173 | fclose($stream); 174 | } 175 | 176 | #[Test] 177 | public function it_can_override_a_file() 178 | { 179 | $this->gitlabAdapter->write('testing.md', '# Testing create', new Config()); 180 | $this->gitlabAdapter->write('testing.md', '# Testing update', new Config()); 181 | 182 | $this->assertStringStartsWith($this->gitlabAdapter->read('testing.md'), '# Testing update'); 183 | 184 | $this->gitlabAdapter->delete('testing.md'); 185 | } 186 | 187 | #[Test] 188 | public function it_can_override_with_a_file_stream() 189 | { 190 | $stream = fopen(__DIR__.'/assets/testing.txt', 'r+'); 191 | $this->gitlabAdapter->writeStream('testing.txt', $stream, new Config()); 192 | fclose($stream); 193 | 194 | $stream = fopen(__DIR__.'/assets/testing-update.txt', 'r+'); 195 | $this->gitlabAdapter->writeStream('testing.txt', $stream, new Config()); 196 | fclose($stream); 197 | 198 | $this->assertTrue($this->gitlabAdapter->fileExists('testing.txt')); 199 | $this->assertEquals('File for testing file streams!', $this->gitlabAdapter->read('testing.txt')); 200 | 201 | $this->gitlabAdapter->delete('testing.txt'); 202 | } 203 | 204 | #[Test] 205 | public function it_can_move_a_file() 206 | { 207 | $this->gitlabAdapter->write('testing.md', '# Testing move', new Config()); 208 | 209 | $this->gitlabAdapter->move('testing.md', 'testing_move.md', new Config()); 210 | 211 | $this->assertFalse($this->gitlabAdapter->fileExists('testing.md')); 212 | $this->assertTrue($this->gitlabAdapter->fileExists('testing_move.md')); 213 | 214 | $this->assertEquals('# Testing move', $this->gitlabAdapter->read('testing_move.md')); 215 | 216 | $this->gitlabAdapter->delete('testing_move.md'); 217 | } 218 | 219 | #[Test] 220 | public function it_throws_when_move_failed() 221 | { 222 | $this->setInvalidProjectId(); 223 | 224 | $this->expectException(UnableToMoveFile::class); 225 | 226 | $this->gitlabAdapter->move('testing_move.md', 'testing.md', new Config()); 227 | } 228 | 229 | #[Test] 230 | public function it_can_copy_a_file() 231 | { 232 | $this->gitlabAdapter->write('testing.md', '# Testing copy', new Config()); 233 | 234 | $this->gitlabAdapter->copy('testing.md', 'testing_copy.md', new Config()); 235 | 236 | $this->assertTrue($this->gitlabAdapter->fileExists('testing.md')); 237 | $this->assertTrue($this->gitlabAdapter->fileExists('testing_copy.md')); 238 | 239 | $this->assertEquals($this->gitlabAdapter->read('testing.md'), '# Testing copy'); 240 | $this->assertEquals($this->gitlabAdapter->read('testing_copy.md'), '# Testing copy'); 241 | 242 | $this->gitlabAdapter->delete('testing.md'); 243 | $this->gitlabAdapter->delete('testing_copy.md'); 244 | } 245 | 246 | #[Test] 247 | public function it_throws_when_copy_failed() 248 | { 249 | $this->setInvalidProjectId(); 250 | 251 | $this->expectException(UnableToCopyFile::class); 252 | 253 | $this->gitlabAdapter->copy('testing_copy.md', 'testing.md', new Config()); 254 | } 255 | 256 | #[Test] 257 | public function it_can_create_a_directory() 258 | { 259 | $this->gitlabAdapter->createDirectory('/testing', new Config()); 260 | 261 | $this->assertTrue($this->gitlabAdapter->fileExists('/testing/.gitkeep')); 262 | 263 | $this->gitlabAdapter->delete('/testing/.gitkeep'); 264 | } 265 | 266 | #[Test] 267 | public function it_can_retrieve_a_list_of_contents_of_root() 268 | { 269 | $list = $this->gitlabAdapter->listContents('/', false); 270 | $expectedPaths = [ 271 | ['type' => 'dir', 'path' => 'recursive'], 272 | ['type' => 'file', 'path' => 'LICENSE'], 273 | ['type' => 'file', 'path' => 'README.md'], 274 | ['type' => 'file', 'path' => 'test'], 275 | ['type' => 'file', 'path' => 'test2'], 276 | ]; 277 | 278 | foreach ($list as $item) { 279 | $this->assertInstanceOf(StorageAttributes::class, $item); 280 | $this->assertContains( 281 | ['type' => $item['type'], 'path' => $item['path']], $expectedPaths 282 | ); 283 | } 284 | } 285 | 286 | #[Test] 287 | public function it_can_retrieve_a_list_of_contents_of_root_recursive() 288 | { 289 | $list = $this->gitlabAdapter->listContents('/', true); 290 | $expectedPaths = [ 291 | ['type' => 'dir', 'path' => 'recursive'], 292 | ['type' => 'dir', 'path' => 'recursive/level-1'], 293 | ['type' => 'dir', 'path' => 'recursive/level-1/level-2'], 294 | ['type' => 'file', 'path' => 'LICENSE'], 295 | ['type' => 'file', 'path' => 'README.md'], 296 | ['type' => 'file', 'path' => 'recursive/recursive.testing.md'], 297 | ['type' => 'file', 'path' => 'recursive/level-1/level-2/.gitkeep'], 298 | ['type' => 'file', 'path' => 'test'], 299 | ['type' => 'file', 'path' => 'test2'], 300 | ]; 301 | 302 | foreach ($list as $item) { 303 | $this->assertInstanceOf(StorageAttributes::class, $item); 304 | $this->assertContains( 305 | ['type' => $item['type'], 'path' => $item['path']], $expectedPaths 306 | ); 307 | } 308 | } 309 | 310 | #[Test] 311 | public function it_can_retrieve_a_list_of_contents_of_sub_folder() 312 | { 313 | $list = $this->gitlabAdapter->listContents('/recursive', false); 314 | $expectedPaths = [ 315 | ['type' => 'dir', 'path' => 'recursive/level-1'], 316 | ['type' => 'dir', 'path' => 'recursive/level-1/level-2'], 317 | ['type' => 'file', 'path' => 'recursive/recursive.testing.md'], 318 | ['type' => 'file', 'path' => 'recursive/level-1/level-2/.gitkeep'], 319 | ]; 320 | 321 | foreach ($list as $item) { 322 | $this->assertInstanceOf(StorageAttributes::class, $item); 323 | $this->assertContains( 324 | ['type' => $item['type'], 'path' => $item['path']], $expectedPaths 325 | ); 326 | } 327 | } 328 | 329 | #[Test] 330 | public function it_can_retrieve_a_list_of_contents_of_deep_sub_folder() 331 | { 332 | $list = $this->gitlabAdapter->listContents('/recursive/level-1/level-2', false); 333 | $expectedPaths = [ 334 | ['type' => 'file', 'path' => 'recursive/level-1/level-2/.gitkeep'], 335 | ]; 336 | 337 | foreach ($list as $item) { 338 | $this->assertInstanceOf(StorageAttributes::class, $item); 339 | $this->assertContains( 340 | ['type' => $item['type'], 'path' => $item['path']], $expectedPaths 341 | ); 342 | } 343 | } 344 | 345 | #[Test] 346 | public function it_can_delete_a_directory() 347 | { 348 | $this->gitlabAdapter->createDirectory('/testing', new Config()); 349 | $this->gitlabAdapter->write('/testing/testing.md', 'Testing delete directory', new Config()); 350 | 351 | $this->gitlabAdapter->deleteDirectory('/testing'); 352 | 353 | $this->assertFalse($this->gitlabAdapter->fileExists('/testing/.gitkeep')); 354 | $this->assertFalse($this->gitlabAdapter->fileExists('/testing/testing.md')); 355 | } 356 | 357 | #[Test] 358 | public function it_throws_when_delete_directory_failed() 359 | { 360 | $this->setInvalidProjectId(); 361 | 362 | $this->expectException(FilesystemException::class); 363 | 364 | $this->gitlabAdapter->deleteDirectory('/testing'); 365 | } 366 | 367 | #[Test] 368 | public function it_can_retrieve_size() 369 | { 370 | $size = $this->gitlabAdapter->fileSize('README.md'); 371 | 372 | $this->assertInstanceOf(FileAttributes::class, $size); 373 | $this->assertEquals(37, $size->fileSize()); 374 | } 375 | 376 | #[Test] 377 | public function it_can_retrieve_mimetype() 378 | { 379 | $metadata = $this->gitlabAdapter->mimeType('README.md'); 380 | 381 | $this->assertInstanceOf(FileAttributes::class, $metadata); 382 | $this->assertEquals('text/markdown', $metadata->mimeType()); 383 | } 384 | 385 | #[Test] 386 | public function it_can_not_retrieve_lastModified() 387 | { 388 | $lastModified = $this->gitlabAdapter->lastModified('README.md'); 389 | 390 | $this->assertInstanceOf(FileAttributes::class, $lastModified); 391 | $this->assertEquals(1606750652, $lastModified->lastModified()); 392 | } 393 | 394 | #[Test] 395 | public function it_throws_when_getting_visibility() 396 | { 397 | $this->expectException(UnableToSetVisibility::class); 398 | 399 | $this->gitlabAdapter->visibility('README.md'); 400 | } 401 | 402 | #[Test] 403 | public function it_throws_when_setting_visibility() 404 | { 405 | $this->expectException(UnableToSetVisibility::class); 406 | 407 | $this->gitlabAdapter->setVisibility('README.md', 0777); 408 | } 409 | 410 | #[Test] 411 | public function it_can_check_directory_if_exists() 412 | { 413 | $dir = 'test-dir/test-dir2/test-dir3'; 414 | $this->gitlabAdapter->createDirectory($dir, new Config()); 415 | $this->assertTrue($this->gitlabAdapter->directoryExists($dir)); 416 | $this->gitlabAdapter->deleteDirectory($dir); 417 | } 418 | 419 | #[Test] 420 | public function it_cannot_check_if_directory_exists() 421 | { 422 | $this->assertFalse($this->gitlabAdapter->directoryExists('test_non_existent_dir')); 423 | } 424 | 425 | private function setInvalidToken() 426 | { 427 | $client = $this->gitlabAdapter->getClient(); 428 | $client->setPersonalAccessToken('123'); 429 | $this->gitlabAdapter->setClient($client); 430 | } 431 | 432 | private function setInvalidProjectId() 433 | { 434 | $client = $this->gitlabAdapter->getClient(); 435 | $client->setProjectId('123'); 436 | $this->gitlabAdapter->setClient($client); 437 | } 438 | } 439 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | config = require(__DIR__.'/config/config.testing.php'); 16 | } 17 | 18 | protected function getClientInstance(): Client 19 | { 20 | return new Client($this->config[ 'project-id' ], $this->config[ 'branch' ], $this->config[ 'base-url' ], 21 | $this->config[ 'personal-access-token' ]); 22 | } 23 | 24 | protected function getAdapterInstance(): GitlabAdapter 25 | { 26 | return new GitlabAdapter($this->getClientInstance()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/assets/testing-update.txt: -------------------------------------------------------------------------------- 1 | File for testing file streams! -------------------------------------------------------------------------------- /tests/assets/testing.txt: -------------------------------------------------------------------------------- 1 | File for testing file streams -------------------------------------------------------------------------------- /tests/config/config.testing.example.php: -------------------------------------------------------------------------------- 1 | 'your-access-token', 21 | 22 | /** 23 | * Project id of your repo 24 | */ 25 | 'project-id' => 'your-project-id', 26 | 27 | /** 28 | * Branch that should be used 29 | */ 30 | 'branch' => 'master', 31 | 32 | /** 33 | * Base URL of Gitlab server you want to use 34 | */ 35 | 'base-url' => 'https://gitlab.com', 36 | ]; 37 | --------------------------------------------------------------------------------