├── composer.json ├── README.md ├── LICENSE └── src ├── StreamableUpload.php └── GoogleDriveAdapter.php /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "masbug/flysystem-google-drive-ext", 3 | "description": "Flysystem adapter for Google Drive with seamless virtual<=>display path translation", 4 | "keywords": [ 5 | "flysystem", 6 | "google-drive", 7 | "translated", 8 | "extended", 9 | "laravel" 10 | ], 11 | "license": "Apache-2.0", 12 | "authors": [ 13 | { 14 | "name": "Naoki Sawada", 15 | "email": "hypweb@gmail.com" 16 | }, 17 | { 18 | "name": "Mitja Spes", 19 | "email": "mitja@lxnav.com" 20 | } 21 | ], 22 | "require": { 23 | "php": "^7.2 | ^8.0", 24 | "ext-mbstring": "*", 25 | "guzzlehttp/guzzle": "^6.3 | ^7.0", 26 | "league/flysystem": "^2.1.1|^3.0", 27 | "google/apiclient": "^2.2", 28 | "guzzlehttp/psr7": "^1.7|^2.0" 29 | }, 30 | "require-dev": { 31 | "phpunit/phpunit": "^8.0 | ^9.3", 32 | "league/flysystem-adapter-test-utilities": "^2.0|^3.0" 33 | }, 34 | "autoload": { 35 | "psr-4": { 36 | "Masbug\\Flysystem\\": "src" 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flysystem adapter for Google Drive with seamless virtual<=>display path translation 2 | 3 | [![Flysystem API version](https://img.shields.io/badge/Flysystem%20API-V2-blue?style=flat-square)](https://github.com/thephpleague/flysystem/) 4 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/masbug/flysystem-google-drive-ext.svg?style=flat-square)](https://packagist.org/packages/masbug/flysystem-google-drive-ext) 5 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg?style=flat-square)](https://opensource.org/licenses/Apache-2.0) 6 | [![Build Status](https://img.shields.io/travis/com/masbug/flysystem-google-drive-ext/2.x.svg?style=flat-square)](https://travis-ci.com/masbug/flysystem-google-drive-ext) 7 | [![StyleCI](https://styleci.io/repos/113434522/shield?branch=2.x)](https://styleci.io/repos/113434522) 8 | [![Total Downloads](https://img.shields.io/packagist/dt/masbug/flysystem-google-drive-ext.svg?style=flat-square)](https://packagist.org/packages/masbug/flysystem-google-drive-ext) 9 | 10 | Google uses unique IDs for each folder and file. This makes it difficult to integrate with other storage services which use normal paths. 11 | 12 | This [Flysystem adapter](https://github.com/thephpleague/flysystem) works around that problem by seamlessly translating paths from "display paths" to "virtual paths", and vice versa. 13 | 14 | For example: virtual path `/Xa3X9GlR6EmbnY1RLVTk5VUtOVkk/0B3X9GlR6EmbnY1RLVTk5VUtOVkk` becomes `/My Nice Dir/myFile.ext` and all ID handling is hidden. 15 | 16 | ## Installation 17 | 18 | - For **Flysystem V2/V3** or **Laravel >= 9.x.x** 19 | 20 | ```bash 21 | composer require masbug/flysystem-google-drive-ext 22 | ``` 23 | 24 | - For **Flysystem V1** or **Laravel <= 8.x.x** use 1.x.x version of the package 25 | 26 | ```bash 27 | composer require masbug/flysystem-google-drive-ext:"^1.0.0" 28 | ``` 29 | 30 | ## Getting Google Keys 31 | 32 | #### Please follow [Google Docs](https://developers.google.com/drive/v3/web/enable-sdk) to obtain your `client ID, client secret & refresh token`. 33 | 34 | #### In addition you can also check these easy-to-follow tutorial by [@ivanvermeyen](https://github.com/ivanvermeyen/laravel-google-drive-demo) 35 | 36 | - [Getting your Client ID and Secret](https://github.com/ivanvermeyen/laravel-google-drive-demo/blob/master/README/1-getting-your-dlient-id-and-secret.md) 37 | - [Getting your Refresh Token](https://github.com/ivanvermeyen/laravel-google-drive-demo/blob/master/README/2-getting-your-refresh-token.md) 38 | 39 | ## Usage 40 | 41 | ```php 42 | $client = new \Google\Client(); 43 | $client->setClientId([client_id]); 44 | $client->setClientSecret([client_secret]); 45 | $client->refreshToken([refresh_token]); 46 | $client->setApplicationName('My Google Drive App'); 47 | 48 | $service = new \Google\Service\Drive($client); 49 | 50 | // variant 1 51 | $adapter = new \Masbug\Flysystem\GoogleDriveAdapter($service, 'My_App_Root'); 52 | 53 | // variant 2: with extra options and query parameters 54 | $adapter2 = new \Masbug\Flysystem\GoogleDriveAdapter( 55 | $service, 56 | 'My_App_Root', 57 | [ 58 | 'useDisplayPaths' => true, /* this is the default */ 59 | 60 | /* These are global parameters sent to server along with per API parameters. Please see https://cloud.google.com/apis/docs/system-parameters for more info. */ 61 | 'parameters' => [ 62 | /* This example tells the remote server to perform quota checks per unique user id. Otherwise the quota would be per client IP. */ 63 | 'quotaUser' => (string)$some_unique_per_user_id 64 | ] 65 | ] 66 | ); 67 | 68 | // variant 3: connect to team drive 69 | $adapter3 = new \Masbug\Flysystem\GoogleDriveAdapter( 70 | $service, 71 | 'My_App_Root', 72 | [ 73 | 'teamDriveId' => '0GF9IioKDqJsRGk9PVA' 74 | ] 75 | ); 76 | 77 | // variant 4: connect to a folder shared with you 78 | $adapter4 = new \Masbug\Flysystem\GoogleDriveAdapter( 79 | $service, 80 | 'My_App_Root', 81 | [ 82 | 'sharedFolderId' => '0GF9IioKDqJsRGk9PVA' 83 | ] 84 | ); 85 | 86 | $fs = new \League\Flysystem\Filesystem($adapter, new \League\Flysystem\Config([\League\Flysystem\Config::OPTION_VISIBILITY => \League\Flysystem\Visibility::PRIVATE])); 87 | ``` 88 | 89 | ```php 90 | // List selected root folder contents 91 | $contents = $fs->listContents('', true /* is_recursive */); 92 | 93 | // List specific folder contents 94 | $contents = $fs->listContents('MyFolder', true /* is_recursive */); 95 | ``` 96 | 97 | ##### File upload 98 | 99 | ```php 100 | // Upload a file 101 | $local_filepath = '/home/user/downloads/file_to_upload.ext'; 102 | $remote_filepath = 'MyFolder/file.ext'; 103 | 104 | $localAdapter = new \League\Flysystem\Local\LocalFilesystemAdapter('/'); 105 | $localfs = new \League\Flysystem\Filesystem($localAdapter, [\League\Flysystem\Config::OPTION_VISIBILITY => \League\Flysystem\Visibility::PRIVATE]); 106 | 107 | try { 108 | $time = Carbon::now(); 109 | $fs->writeStream($remote_filepath, $localfs->readStream($local_filepath), new \League\Flysystem\Config()); 110 | 111 | $speed = !(float)$time->diffInSeconds() ? 0 :filesize($local_filepath) / (float)$time->diffInSeconds(); 112 | echo 'Elapsed time: '.$time->diffForHumans(null, true).PHP_EOL; 113 | echo 'Speed: '. number_format($speed/1024,2) . ' KB/s'.PHP_EOL; 114 | } catch(\League\Flysystem\UnableToWriteFile $e) { 115 | echo 'UnableToWriteFile!'.PHP_EOL.$e->getMessage(); 116 | } 117 | 118 | // NOTE: Remote folders are automatically created. 119 | ``` 120 | 121 | ##### File download 122 | 123 | ```php 124 | // Download a file 125 | $remote_filepath = 'MyFolder/file.ext'; 126 | $local_filepath = '/home/user/downloads/file.ext'; 127 | 128 | $localAdapter = new \League\Flysystem\Local\LocalFilesystemAdapter('/'); 129 | $localfs = new \League\Flysystem\Filesystem($localAdapter, [\League\Flysystem\Config::OPTION_VISIBILITY => \League\Flysystem\Visibility::PRIVATE]); 130 | 131 | try { 132 | $time = Carbon::now(); 133 | $localfs->writeStream($local_filepath, $fs->readStream($remote_filepath), new \League\Flysystem\Config()); 134 | 135 | $speed = !(float)$time->diffInSeconds() ? 0 :filesize($local_filepath) / (float)$time->diffInSeconds(); 136 | echo 'Elapsed time: '.$time->diffForHumans(null, true).PHP_EOL; 137 | echo 'Speed: '. number_format($speed/1024,2) . ' KB/s'.PHP_EOL; 138 | } catch(\League\Flysystem\UnableToWriteFile $e) { 139 | echo 'UnableToWriteFile!'.PHP_EOL.$e->getMessage(); 140 | } 141 | ``` 142 | 143 | ##### How to get TeamDrive list and IDs 144 | 145 | ```php 146 | $drives = $fs->getAdapter()->getService()->teamdrives->listTeamdrives()->getTeamDrives(); 147 | foreach ($drives as $drive) { 148 | echo 'TeamDrive: ' . $drive->name . PHP_EOL; 149 | echo 'ID: ' . $drive->id . PHP_EOL. PHP_EOL; 150 | } 151 | ``` 152 | 153 | ##### How permanently deletes all of the user's trashed files 154 | ```php 155 | $fs->getAdapter()->emptyTrash([]); 156 | ``` 157 | 158 | ## Using with Laravel Framework 159 | 160 | ##### Update `.env` file with google keys 161 | 162 | Add the keys you created to your `.env` file and set `google` as your default cloud storage. You can copy the `.env.example` file and fill in the blanks. 163 | 164 | ``` 165 | FILESYSTEM_CLOUD=google 166 | GOOGLE_DRIVE_CLIENT_ID=xxx.apps.googleusercontent.com 167 | GOOGLE_DRIVE_CLIENT_SECRET=xxx 168 | GOOGLE_DRIVE_REFRESH_TOKEN=xxx 169 | GOOGLE_DRIVE_FOLDER= 170 | #GOOGLE_DRIVE_TEAM_DRIVE_ID=xxx 171 | #GOOGLE_DRIVE_SHARED_FOLDER_ID=xxx 172 | 173 | # you can use more accounts, only add more configs 174 | #SECOND_GOOGLE_DRIVE_CLIENT_ID=xxx.apps.googleusercontent.com 175 | #SECOND_GOOGLE_DRIVE_CLIENT_SECRET=xxx 176 | #SECOND_GOOGLE_DRIVE_REFRESH_TOKEN=xxx 177 | #SECOND_GOOGLE_DRIVE_FOLDER=backups 178 | #SECOND_DRIVE_TEAM_DRIVE_ID=xxx 179 | #SECOND_DRIVE_SHARED_FOLDER_ID=xxx 180 | ``` 181 | 182 | ##### Add disks on `config/filesystems.php` 183 | 184 | ```php 185 | 'disks' => [ 186 | // ... 187 | 'google' => [ 188 | 'driver' => 'google', 189 | 'clientId' => env('GOOGLE_DRIVE_CLIENT_ID'), 190 | 'clientSecret' => env('GOOGLE_DRIVE_CLIENT_SECRET'), 191 | 'refreshToken' => env('GOOGLE_DRIVE_REFRESH_TOKEN'), 192 | 'folder' => env('GOOGLE_DRIVE_FOLDER'), // without folder is root of drive or team drive 193 | //'teamDriveId' => env('GOOGLE_DRIVE_TEAM_DRIVE_ID'), 194 | //'sharedFolderId' => env('GOOGLE_DRIVE_SHARED_FOLDER_ID'), 195 | ], 196 | // you can use more accounts, only add more disks and configs on .env 197 | // also you can use the same account and point to a diferent folders for each disk 198 | /*'second_google' => [ 199 | 'driver' => 'google', 200 | 'clientId' => env('SECOND_GOOGLE_DRIVE_CLIENT_ID'), 201 | 'clientSecret' => env('SECOND_GOOGLE_DRIVE_CLIENT_SECRET'), 202 | 'refreshToken' => env('SECOND_GOOGLE_DRIVE_REFRESH_TOKEN'), 203 | 'folder' => env('SECOND_GOOGLE_DRIVE_FOLDER'), 204 | ],*/ 205 | // ... 206 | ], 207 | ``` 208 | 209 | ##### Add driver storage in a `ServiceProvider` on path `app/Providers/` 210 | 211 | Example: 212 | 213 | ```php 214 | namespace App\Providers; 215 | 216 | use Illuminate\Support\Facades\Storage; 217 | use Illuminate\Support\ServiceProvider; 218 | 219 | class AppServiceProvider extends ServiceProvider { // can be a custom ServiceProvider 220 | // ... 221 | public function boot(){ 222 | // ... 223 | try { 224 | \Storage::extend('google', function($app, $config) { 225 | $options = []; 226 | 227 | if (!empty($config['teamDriveId'] ?? null)) { 228 | $options['teamDriveId'] = $config['teamDriveId']; 229 | } 230 | 231 | if (!empty($config['sharedFolderId'] ?? null)) { 232 | $options['sharedFolderId'] = $config['sharedFolderId']; 233 | } 234 | 235 | $client = new \Google\Client(); 236 | $client->setClientId($config['clientId']); 237 | $client->setClientSecret($config['clientSecret']); 238 | $client->refreshToken($config['refreshToken']); 239 | 240 | $service = new \Google\Service\Drive($client); 241 | $adapter = new \Masbug\Flysystem\GoogleDriveAdapter($service, $config['folder'] ?? '/', $options); 242 | $driver = new \League\Flysystem\Filesystem($adapter); 243 | 244 | return new \Illuminate\Filesystem\FilesystemAdapter($driver, $adapter); 245 | }); 246 | } catch(\Exception $e) { 247 | // your exception handling logic 248 | } 249 | // ... 250 | } 251 | // ... 252 | } 253 | ``` 254 | 255 | Now you can access the drives like so: 256 | 257 | ```php 258 | $googleDisk = Storage::disk('google'); 259 | //$secondDisk = Storage::disk('second_google'); //others disks 260 | ``` 261 | 262 | Keep in mind that there can only be one default cloud storage drive, defined by `FILESYSTEM_CLOUD` in your `.env` (or config) file. If you set it to `google`, that will be the cloud drive: 263 | 264 | ```php 265 | Storage::cloud(); // refers to Storage::disk('google') 266 | ``` 267 | 268 | ## Limitations 269 | 270 | Using display paths as identifiers for folders and files requires them to be unique. Unfortunately Google Drive allows users to create files and folders with same (displayed) names. In such cases when unique path cannot be determined this adapter chooses the oldest (first) instance. 271 | In case the newer duplicate is a folder and user puts a unique file or folder inside the adapter will be able to reach it properly (because full path is unique). 272 | 273 | Concurrent use of same Google Drive might lead to unexpected problems due to heavy caching of file/folder identifiers and file objects. 274 | 275 | ## Acknowledgements 276 | 277 | This adapter is based on wonderful [flysystem-google-drive](https://github.com/nao-pon/flysystem-google-drive) by Naoki Sawada. 278 | 279 | It also contains an adaptation of [Google_Http_MediaFileUpload](https://github.com/googleapis/google-api-php-client/blob/master/src/Http/MediaFileUpload.php) by Google. I've added support for resumable uploads directly from streams (avoiding copying data to memory). 280 | 281 | TeamDrive support was implemented by Maximilian Ruta - [Deltachaos](https://github.com/Deltachaos). 282 | 283 | Adapter rewrite for Flysystem V2 and various fixes were implemented by Erik Niebla - [erikn69](https://github.com/erikn69). 284 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ----------------------- 2 | GoogleDriveAdapter.php: 3 | ----------------------- 4 | 5 | The MIT License (MIT) 6 | 7 | Modified work Copyright (c) 2017 Mitja Spes 8 | Original work Copyright (c) 2016 nao-pon Hypweb.net 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all 18 | copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | SOFTWARE. 27 | 28 | ----------------------- 29 | StreamableUpload.php: 30 | ----------------------- 31 | Modified work Copyright (c) 2017 Mitja Spes 32 | Original work Copyright 2012 Google Inc. 33 | 34 | Apache License 35 | Version 2.0, January 2004 36 | http://www.apache.org/licenses/ 37 | 38 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 39 | 40 | 1. Definitions. 41 | 42 | "License" shall mean the terms and conditions for use, reproduction, 43 | and distribution as defined by Sections 1 through 9 of this document. 44 | 45 | "Licensor" shall mean the copyright owner or entity authorized by 46 | the copyright owner that is granting the License. 47 | 48 | "Legal Entity" shall mean the union of the acting entity and all 49 | other entities that control, are controlled by, or are under common 50 | control with that entity. For the purposes of this definition, 51 | "control" means (i) the power, direct or indirect, to cause the 52 | direction or management of such entity, whether by contract or 53 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 54 | outstanding shares, or (iii) beneficial ownership of such entity. 55 | 56 | "You" (or "Your") shall mean an individual or Legal Entity 57 | exercising permissions granted by this License. 58 | 59 | "Source" form shall mean the preferred form for making modifications, 60 | including but not limited to software source code, documentation 61 | source, and configuration files. 62 | 63 | "Object" form shall mean any form resulting from mechanical 64 | transformation or translation of a Source form, including but 65 | not limited to compiled object code, generated documentation, 66 | and conversions to other media types. 67 | 68 | "Work" shall mean the work of authorship, whether in Source or 69 | Object form, made available under the License, as indicated by a 70 | copyright notice that is included in or attached to the work 71 | (an example is provided in the Appendix below). 72 | 73 | "Derivative Works" shall mean any work, whether in Source or Object 74 | form, that is based on (or derived from) the Work and for which the 75 | editorial revisions, annotations, elaborations, or other modifications 76 | represent, as a whole, an original work of authorship. For the purposes 77 | of this License, Derivative Works shall not include works that remain 78 | separable from, or merely link (or bind by name) to the interfaces of, 79 | the Work and Derivative Works thereof. 80 | 81 | "Contribution" shall mean any work of authorship, including 82 | the original version of the Work and any modifications or additions 83 | to that Work or Derivative Works thereof, that is intentionally 84 | submitted to Licensor for inclusion in the Work by the copyright owner 85 | or by an individual or Legal Entity authorized to submit on behalf of 86 | the copyright owner. For the purposes of this definition, "submitted" 87 | means any form of electronic, verbal, or written communication sent 88 | to the Licensor or its representatives, including but not limited to 89 | communication on electronic mailing lists, source code control systems, 90 | and issue tracking systems that are managed by, or on behalf of, the 91 | Licensor for the purpose of discussing and improving the Work, but 92 | excluding communication that is conspicuously marked or otherwise 93 | designated in writing by the copyright owner as "Not a Contribution." 94 | 95 | "Contributor" shall mean Licensor and any individual or Legal Entity 96 | on behalf of whom a Contribution has been received by Licensor and 97 | subsequently incorporated within the Work. 98 | 99 | 2. Grant of Copyright License. Subject to the terms and conditions of 100 | this License, each Contributor hereby grants to You a perpetual, 101 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 102 | copyright license to reproduce, prepare Derivative Works of, 103 | publicly display, publicly perform, sublicense, and distribute the 104 | Work and such Derivative Works in Source or Object form. 105 | 106 | 3. Grant of Patent License. Subject to the terms and conditions of 107 | this License, each Contributor hereby grants to You a perpetual, 108 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 109 | (except as stated in this section) patent license to make, have made, 110 | use, offer to sell, sell, import, and otherwise transfer the Work, 111 | where such license applies only to those patent claims licensable 112 | by such Contributor that are necessarily infringed by their 113 | Contribution(s) alone or by combination of their Contribution(s) 114 | with the Work to which such Contribution(s) was submitted. If You 115 | institute patent litigation against any entity (including a 116 | cross-claim or counterclaim in a lawsuit) alleging that the Work 117 | or a Contribution incorporated within the Work constitutes direct 118 | or contributory patent infringement, then any patent licenses 119 | granted to You under this License for that Work shall terminate 120 | as of the date such litigation is filed. 121 | 122 | 4. Redistribution. You may reproduce and distribute copies of the 123 | Work or Derivative Works thereof in any medium, with or without 124 | modifications, and in Source or Object form, provided that You 125 | meet the following conditions: 126 | 127 | (a) You must give any other recipients of the Work or 128 | Derivative Works a copy of this License; and 129 | 130 | (b) You must cause any modified files to carry prominent notices 131 | stating that You changed the files; and 132 | 133 | (c) You must retain, in the Source form of any Derivative Works 134 | that You distribute, all copyright, patent, trademark, and 135 | attribution notices from the Source form of the Work, 136 | excluding those notices that do not pertain to any part of 137 | the Derivative Works; and 138 | 139 | (d) If the Work includes a "NOTICE" text file as part of its 140 | distribution, then any Derivative Works that You distribute must 141 | include a readable copy of the attribution notices contained 142 | within such NOTICE file, excluding those notices that do not 143 | pertain to any part of the Derivative Works, in at least one 144 | of the following places: within a NOTICE text file distributed 145 | as part of the Derivative Works; within the Source form or 146 | documentation, if provided along with the Derivative Works; or, 147 | within a display generated by the Derivative Works, if and 148 | wherever such third-party notices normally appear. The contents 149 | of the NOTICE file are for informational purposes only and 150 | do not modify the License. You may add Your own attribution 151 | notices within Derivative Works that You distribute, alongside 152 | or as an addendum to the NOTICE text from the Work, provided 153 | that such additional attribution notices cannot be construed 154 | as modifying the License. 155 | 156 | You may add Your own copyright statement to Your modifications and 157 | may provide additional or different license terms and conditions 158 | for use, reproduction, or distribution of Your modifications, or 159 | for any such Derivative Works as a whole, provided Your use, 160 | reproduction, and distribution of the Work otherwise complies with 161 | the conditions stated in this License. 162 | 163 | 5. Submission of Contributions. Unless You explicitly state otherwise, 164 | any Contribution intentionally submitted for inclusion in the Work 165 | by You to the Licensor shall be under the terms and conditions of 166 | this License, without any additional terms or conditions. 167 | Notwithstanding the above, nothing herein shall supersede or modify 168 | the terms of any separate license agreement you may have executed 169 | with Licensor regarding such Contributions. 170 | 171 | 6. Trademarks. This License does not grant permission to use the trade 172 | names, trademarks, service marks, or product names of the Licensor, 173 | except as required for reasonable and customary use in describing the 174 | origin of the Work and reproducing the content of the NOTICE file. 175 | 176 | 7. Disclaimer of Warranty. Unless required by applicable law or 177 | agreed to in writing, Licensor provides the Work (and each 178 | Contributor provides its Contributions) on an "AS IS" BASIS, 179 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 180 | implied, including, without limitation, any warranties or conditions 181 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 182 | PARTICULAR PURPOSE. You are solely responsible for determining the 183 | appropriateness of using or redistributing the Work and assume any 184 | risks associated with Your exercise of permissions under this License. 185 | 186 | 8. Limitation of Liability. In no event and under no legal theory, 187 | whether in tort (including negligence), contract, or otherwise, 188 | unless required by applicable law (such as deliberate and grossly 189 | negligent acts) or agreed to in writing, shall any Contributor be 190 | liable to You for damages, including any direct, indirect, special, 191 | incidental, or consequential damages of any character arising as a 192 | result of this License or out of the use or inability to use the 193 | Work (including but not limited to damages for loss of goodwill, 194 | work stoppage, computer failure or malfunction, or any and all 195 | other commercial damages or losses), even if such Contributor 196 | has been advised of the possibility of such damages. 197 | 198 | 9. Accepting Warranty or Additional Liability. While redistributing 199 | the Work or Derivative Works thereof, You may choose to offer, 200 | and charge a fee for, acceptance of support, warranty, indemnity, 201 | or other liability obligations and/or rights consistent with this 202 | License. However, in accepting such obligations, You may act only 203 | on Your own behalf and on Your sole responsibility, not on behalf 204 | of any other Contributor, and only if You agree to indemnify, 205 | defend, and hold each Contributor harmless for any liability 206 | incurred by, or claims asserted against, such Contributor by reason 207 | of your accepting any such warranty or additional liability. 208 | 209 | END OF TERMS AND CONDITIONS 210 | 211 | APPENDIX: How to apply the Apache License to your work. 212 | 213 | To apply the Apache License to your work, attach the following 214 | boilerplate notice, with the fields enclosed by brackets "[]" 215 | replaced with your own identifying information. (Don't include 216 | the brackets!) The text should be enclosed in the appropriate 217 | comment syntax for the file format. We also recommend that a 218 | file or class name and description of purpose be included on the 219 | same "printed page" as the copyright notice for easier 220 | identification within third-party archives. 221 | 222 | Copyright [yyyy] [name of copyright owner] 223 | 224 | Licensed under the Apache License, Version 2.0 (the "License"); 225 | you may not use this file except in compliance with the License. 226 | You may obtain a copy of the License at 227 | 228 | http://www.apache.org/licenses/LICENSE-2.0 229 | 230 | Unless required by applicable law or agreed to in writing, software 231 | distributed under the License is distributed on an "AS IS" BASIS, 232 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 233 | See the License for the specific language governing permissions and 234 | limitations under the License. 235 | -------------------------------------------------------------------------------- /src/StreamableUpload.php: -------------------------------------------------------------------------------- 1 | client = $client; 100 | $this->request = $request; 101 | $this->mimeType = $mimeType; 102 | $this->data = $data !== null ? Utils::streamFor($data) : null; 103 | $this->resumable = $resumable; 104 | $this->chunkSize = is_bool($chunkSize) ? 0 : $chunkSize; 105 | $this->progress = 0; 106 | $this->size = '*'; 107 | if ($this->data !== null) { 108 | $size = $this->data->getSize(); 109 | if ($size !== null) { 110 | $this->size = $size; 111 | } 112 | } 113 | 114 | $this->process(); 115 | } 116 | 117 | /** 118 | * Set the size of the file that is being uploaded. 119 | * 120 | * @param int $size file size in bytes 121 | */ 122 | public function setFileSize($size) 123 | { 124 | $this->size = $size; 125 | } 126 | 127 | /** 128 | * Return the progress on the upload 129 | * 130 | * @return int progress in bytes uploaded. 131 | */ 132 | public function getProgress() 133 | { 134 | return $this->progress; 135 | } 136 | 137 | /** 138 | * Send the next part of the file to upload. 139 | * 140 | * @param null|bool|string|StreamInterface $chunk The next set of bytes to send. If stream is provided then chunkSize is ignored. 141 | * If false it will use $this->data set at construct time. 142 | * @return false|mixed 143 | */ 144 | public function nextChunk($chunk = false) 145 | { 146 | $resumeUri = $this->getResumeUri(); 147 | 148 | if ($chunk === null || is_bool($chunk)) { 149 | if ($this->chunkSize < 1) { 150 | throw new \InvalidArgumentException('Invalid chunk size'); 151 | } 152 | if (!$this->data instanceof StreamInterface) { 153 | throw new \InvalidArgumentException('Invalid data stream'); 154 | } 155 | $this->data->seek($this->progress, SEEK_SET); 156 | if ($this->data->eof()) { 157 | return true; // finished 158 | } 159 | $chunk = new LimitStream($this->data, $this->chunkSize, $this->data->tell()); 160 | } else { 161 | $chunk = Utils::streamFor($chunk); 162 | } 163 | $size = $chunk->getSize(); 164 | 165 | if ($size === null) { 166 | throw new \InvalidArgumentException('Chunk doesn\'t support getSize'); 167 | } else { 168 | if ($size < 1) { 169 | return true; // finished 170 | } 171 | 172 | $lastBytePos = $this->progress + $size - 1; 173 | $headers = [ 174 | 'content-range' => 'bytes '.$this->progress.'-'.$lastBytePos.'/'.$this->size, 175 | 'content-length' => $size, 176 | 'expect' => '', 177 | ]; 178 | } 179 | 180 | $request = new Request( 181 | 'PUT', 182 | $resumeUri, 183 | $headers, 184 | $chunk 185 | ); 186 | 187 | return $this->makePutRequest($request); 188 | } 189 | 190 | /** 191 | * Return the HTTP result code from the last call made. 192 | * 193 | * @return int code 194 | */ 195 | public function getHttpResultCode() 196 | { 197 | return $this->httpResultCode; 198 | } 199 | 200 | /** 201 | * Sends a PUT-Request to google drive and parses the response, 202 | * setting the appropriate variables from the response() 203 | * 204 | * @param RequestInterface $request the request which will be sent 205 | * @return false|mixed false when the upload is unfinished or the decoded http response 206 | */ 207 | private function makePutRequest(RequestInterface $request) 208 | { 209 | /** @var ResponseInterface $response */ 210 | $response = $this->client->execute($request); 211 | $this->httpResultCode = $response->getStatusCode(); 212 | 213 | if (308 == $this->httpResultCode) { 214 | // Track the amount uploaded. 215 | $range = $response->getHeaderLine('range'); 216 | if ($range) { 217 | $range_array = explode('-', $range); 218 | $this->progress = $range_array[1] + 1; 219 | } 220 | 221 | // Allow for changing upload URLs. 222 | $location = $response->getHeaderLine('location'); 223 | if ($location) { 224 | $this->resumeUri = $location; 225 | } 226 | 227 | // No problems, but upload not complete. 228 | return false; 229 | } 230 | 231 | return REST::decodeHttpResponse($response, $this->request); 232 | } 233 | 234 | /** 235 | * Resume a previously unfinished upload 236 | * 237 | * @param string $resumeUri The resume-URI of the unfinished, resumable upload. 238 | * @return false|mixed 239 | */ 240 | public function resume($resumeUri) 241 | { 242 | $this->resumeUri = $resumeUri; 243 | $headers = [ 244 | 'content-range' => 'bytes */'.$this->size, 245 | 'content-length' => 0, 246 | ]; 247 | $httpRequest = new Request( 248 | 'PUT', 249 | $this->resumeUri, 250 | $headers 251 | ); 252 | 253 | return $this->makePutRequest($httpRequest); 254 | } 255 | 256 | /** 257 | * @return \Psr\Http\Message\RequestInterface $request 258 | * @visible for testing 259 | */ 260 | private function process() 261 | { 262 | $this->transformToUploadUrl(); 263 | $request = $this->request; 264 | 265 | $postBody = ''; 266 | $contentType = false; 267 | 268 | $meta = (string)$request->getBody(); 269 | $meta = is_string($meta) ? json_decode($meta, true) : $meta; 270 | 271 | $uploadType = $this->getUploadType($meta); 272 | $request = $request->withUri( 273 | Uri::withQueryValue($request->getUri(), 'uploadType', $uploadType) 274 | ); 275 | 276 | $mimeType = $this->mimeType ?: $request->getHeaderLine('content-type'); 277 | 278 | if (self::UPLOAD_RESUMABLE_TYPE == $uploadType) { 279 | $contentType = $mimeType; 280 | $postBody = is_string($meta) ? $meta : json_encode($meta); 281 | } else { 282 | if (self::UPLOAD_MEDIA_TYPE == $uploadType) { 283 | $contentType = $mimeType; 284 | $postBody = $this->data; 285 | } else { 286 | if (self::UPLOAD_MULTIPART_TYPE == $uploadType) { 287 | // This is a multipart/related upload. 288 | $boundary = $this->boundary ?: /* @scrutinizer ignore-call */ mt_rand(); 289 | $boundary = str_replace('"', '', $boundary); 290 | $contentType = 'multipart/related; boundary='.$boundary; 291 | $related = "--$boundary\r\n"; 292 | $related .= "Content-Type: application/json; charset=UTF-8\r\n"; 293 | $related .= "\r\n".json_encode($meta)."\r\n"; 294 | $related .= "--$boundary\r\n"; 295 | $related .= "Content-Type: $mimeType\r\n"; 296 | $related .= "Content-Transfer-Encoding: base64\r\n"; 297 | $related .= "\r\n".base64_encode(/** @scrutinizer ignore-type */ $this->data)."\r\n"; 298 | $related .= "--$boundary--"; 299 | $postBody = $related; 300 | } 301 | } 302 | } 303 | 304 | $request = $request->withBody(Utils::streamFor($postBody)); 305 | 306 | if (isset($contentType) && $contentType) { 307 | $request = $request->withHeader('content-type', $contentType); 308 | } 309 | 310 | return $this->request = $request; 311 | } 312 | 313 | /** 314 | * Valid upload types: 315 | * - resumable (UPLOAD_RESUMABLE_TYPE) 316 | * - media (UPLOAD_MEDIA_TYPE) 317 | * - multipart (UPLOAD_MULTIPART_TYPE) 318 | * 319 | * @param $meta 320 | * @return string 321 | * @visible for testing 322 | */ 323 | public function getUploadType($meta) 324 | { 325 | if ($this->resumable) { 326 | return self::UPLOAD_RESUMABLE_TYPE; 327 | } 328 | 329 | if (false == $meta && $this->data) { 330 | return self::UPLOAD_MEDIA_TYPE; 331 | } 332 | 333 | return self::UPLOAD_MULTIPART_TYPE; 334 | } 335 | 336 | public function getResumeUri() 337 | { 338 | if (null === $this->resumeUri) { 339 | $this->resumeUri = $this->fetchResumeUri(); 340 | } 341 | 342 | return $this->resumeUri; 343 | } 344 | 345 | private function fetchResumeUri() 346 | { 347 | $body = $this->request->getBody(); 348 | if ($body) { 349 | $headers = [ 350 | 'content-type' => 'application/json; charset=UTF-8', 351 | 'content-length' => $body->getSize(), 352 | 'x-upload-content-type' => $this->mimeType, 353 | 'expect' => '', 354 | ]; 355 | if (is_int($this->size)) { 356 | $headers['x-upload-content-length'] = $this->size; 357 | } 358 | 359 | foreach ($headers as $key => $value) { 360 | $this->request = $this->request->withHeader($key, $value); 361 | } 362 | } 363 | 364 | $response = $this->client->execute($this->request, /** @scrutinizer ignore-type */ false); 365 | $location = $response->getHeaderLine('location'); 366 | $code = $response->getStatusCode(); 367 | 368 | if (200 == $code && true == $location) { 369 | return $location; 370 | } 371 | 372 | $message = $code; 373 | $body = json_decode((string)$this->request->getBody(), true); 374 | if (isset($body['error']['errors'])) { 375 | $message .= ': '; 376 | foreach ($body['error']['errors'] as $error) { 377 | $message .= $error['domain'].', '.$error['message'].';'; 378 | } 379 | $message = rtrim($message, ';'); 380 | } 381 | 382 | $error = "Failed to start the resumable upload (HTTP {$message})"; 383 | $this->client->getLogger()->error($error); 384 | 385 | throw new GoogleException($error); 386 | } 387 | 388 | private function transformToUploadUrl() 389 | { 390 | $parts = parse_url((string)$this->request->getUri()); 391 | if (!isset($parts['path'])) { 392 | $parts['path'] = ''; 393 | } 394 | $parts['path'] = '/upload'.$parts['path']; 395 | $uri = Uri::fromParts($parts); 396 | $this->request = $this->request->withUri($uri); 397 | } 398 | 399 | public function setChunkSize($chunkSize) 400 | { 401 | $this->chunkSize = $chunkSize; 402 | } 403 | 404 | public function getRequest() 405 | { 406 | return $this->request; 407 | } 408 | } 409 | -------------------------------------------------------------------------------- /src/GoogleDriveAdapter.php: -------------------------------------------------------------------------------- 1 | 'drive', 95 | 'useHasDir' => false, 96 | 'useDisplayPaths' => true, 97 | 'showDisplayPaths' => false, 98 | 'usePermanentDelete' => false, 99 | 'useSinglePathTransaction' => false, 100 | 'publishPermission' => [ 101 | 'type' => 'anyone', 102 | 'role' => 'reader', 103 | 'withLink' => true 104 | ], 105 | 'appsExportMap' => [ 106 | 'application/vnd.google-apps.document' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 107 | 'application/vnd.google-apps.spreadsheet' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 108 | 'application/vnd.google-apps.drawing' => 'application/pdf', 109 | 'application/vnd.google-apps.presentation' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 110 | 'application/vnd.google-apps.script' => 'application/vnd.google-apps.script+json', 111 | 'default' => 'application/pdf' 112 | ], 113 | 114 | 'parameters' => [], 115 | 116 | 'driveId' => null, 117 | 118 | 'sanitize_chars' => [ 119 | // sanitize filename 120 | // file system reserved https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words 121 | // control characters http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx 122 | // non-printing characters DEL, NO-BREAK SPACE, SOFT HYPHEN 123 | // URI reserved https://tools.ietf.org/html/rfc3986#section-2.2 124 | // URL unsafe characters https://www.ietf.org/rfc/rfc1738.txt 125 | 126 | // must not allow 127 | '/', '\\', '?', '%', '*', ':', '|', '"', '<', '>', 128 | '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', '\x09', '\x0A', '\x0B', '\x0C', '\x0D', '\x0E', '\x0F', 129 | '\x10', '\x11', '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x18', '\x19', '\x1A', '\x1B', '\x1C', '\x1D', '\x1E', '\x1F', 130 | '\x7F', '\xA0', '\xAD', 131 | 132 | // optional 133 | '#', '@', '!', '$', '&', '\'', '+', ';', '=', 134 | '^', '~', '`', 135 | ], 136 | 'sanitize_replacement_char' => '_' 137 | ]; 138 | 139 | /** 140 | * A comma-separated list of spaces to query 141 | * Supported values are 'drive', 'appDataFolder' and 'photos' 142 | * 143 | * @var string 144 | */ 145 | protected $spaces; 146 | 147 | /** 148 | * Root path 149 | * 150 | * @var string 151 | */ 152 | protected $root; 153 | 154 | /** 155 | * Permission array as published item 156 | * 157 | * @var array 158 | */ 159 | protected $publishPermission; 160 | 161 | /** 162 | * Cache of file objects 163 | * 164 | * @var array 165 | */ 166 | private $cacheFileObjects = []; 167 | 168 | /** 169 | * Cache of hasDir 170 | * 171 | * @var array 172 | */ 173 | private $cacheHasDirs = []; 174 | 175 | /** 176 | * Use hasDir function 177 | * 178 | * @var bool 179 | */ 180 | private $useHasDir = false; 181 | 182 | /** 183 | * Permanent delete files and directories, avoid setTrashed 184 | * 185 | * @var bool 186 | */ 187 | private $usePermanentDelete = false; 188 | 189 | /** 190 | * When only needs to upload/download single file, is quick and 191 | * avoid 'Timeout/Allowed memory exhausted' when too many files 192 | * 193 | * @var bool 194 | */ 195 | private $useSinglePathTransaction = false; 196 | 197 | /** 198 | * Options array 199 | * 200 | * @var array 201 | */ 202 | private $options = []; 203 | 204 | /** 205 | * Using display paths instead of virtual IDs 206 | * 207 | * @var bool 208 | */ 209 | private $useDisplayPaths = true; 210 | 211 | /** 212 | * Show display paths in extra metadata instead of virtual IDs 213 | * 214 | * @var bool 215 | */ 216 | private $showDisplayPaths = false; 217 | 218 | /** 219 | * Resolved root ID 220 | * 221 | * @var string 222 | */ 223 | private $rootId = null; 224 | 225 | /** 226 | * Full path => virtual ID cache 227 | * 228 | * @var array 229 | */ 230 | private $cachedPaths = []; 231 | 232 | /** 233 | * Recent virtual ID => file object requests cache 234 | * 235 | * @var array 236 | */ 237 | private $requestedIds = []; 238 | 239 | /** 240 | * @var array Optional parameters sent with each request (see Google_Service_Resource var stackParameters and https://developers.google.com/analytics/devguides/reporting/core/v4/parameters) 241 | */ 242 | private $optParams = []; 243 | 244 | /** 245 | * @var PathPrefixer 246 | */ 247 | private $prefixer; 248 | 249 | /** 250 | * GoogleDriveAdapter constructor. 251 | * 252 | * @param Drive $service 253 | * @param string|null $root 254 | * @param array $options 255 | */ 256 | public function __construct($service, $root = null, $options = []) 257 | { 258 | $this->service = $service; 259 | 260 | $this->options = array_replace_recursive(static::$defaultOptions, $options); 261 | 262 | $this->spaces = $this->options['spaces']; 263 | $this->useHasDir = $this->options['useHasDir']; 264 | $this->usePermanentDelete = $this->options['usePermanentDelete']; 265 | $this->useSinglePathTransaction = $this->options['useSinglePathTransaction']; 266 | $this->publishPermission = $this->options['publishPermission']; 267 | $this->useDisplayPaths = $this->options['useDisplayPaths']; 268 | $this->showDisplayPaths = $this->options['showDisplayPaths']; 269 | $this->optParams = $this->cleanOptParameters($this->options['parameters']); 270 | 271 | if ($root !== null) { 272 | $root = trim($root, '/'); 273 | if ($root === '') { 274 | $root = null; 275 | } 276 | } 277 | 278 | if (isset($this->options['teamDriveId'])) { 279 | $this->root = null; 280 | $this->setTeamDriveId($this->options['teamDriveId']); 281 | if ($this->useDisplayPaths && $root !== null) { 282 | // get real root id 283 | $this->root = $this->toSingleVirtualPath($root, false, true, true, true); 284 | 285 | // reset cache 286 | $this->rootId = $this->root; 287 | $this->clearCache(); 288 | } 289 | } else if (isset($this->options['sharedFolderId'])) { 290 | $this->root = (!$this->useDisplayPaths && $root !== null) 291 | ? $root 292 | : $this->options['sharedFolderId']; 293 | 294 | $this->setPathPrefix(''); 295 | 296 | if ($this->useDisplayPaths && $root !== null) { 297 | // get real root id 298 | $this->root = $this->toSingleVirtualPath($root, false, true, true, true); 299 | // reset cache 300 | $this->rootId = $this->root; 301 | $this->clearCache(); 302 | } 303 | } else { 304 | if (!$this->useDisplayPaths || $root === null) { 305 | if ($root === null) { 306 | $root = $this->spaces === 'appDataFolder' ? 'appDataFolder' : 'root'; 307 | } 308 | $this->root = $root; 309 | $this->setPathPrefix(''); 310 | } else { 311 | $this->root = $this->spaces === 'appDataFolder' ? 'appDataFolder' : 'root'; 312 | $this->setPathPrefix(''); 313 | 314 | // get real root id 315 | $this->root = $this->toSingleVirtualPath($root, false, true, true, true); 316 | 317 | // reset cache 318 | $this->rootId = $this->root; 319 | $this->clearCache(); 320 | } 321 | } 322 | } 323 | 324 | /** 325 | * Gets the service 326 | * 327 | * @return \Google\Service\Drive 328 | */ 329 | public function getService() 330 | { 331 | $this->refreshToken(); 332 | return $this->service; 333 | } 334 | 335 | /** 336 | * Allow to forcefully clear the cache to enable long running process 337 | * 338 | * @return void 339 | */ 340 | public function clearCache() 341 | { 342 | $this->cachedPaths = []; 343 | $this->requestedIds = []; 344 | $this->cacheFileObjects = []; 345 | $this->cacheHasDirs = []; 346 | } 347 | 348 | /** 349 | * Allow to refresh tokens to enable long running process 350 | * 351 | * @return void 352 | */ 353 | public function refreshToken() 354 | { 355 | $client = $this->service->getClient(); 356 | if ($client->isAccessTokenExpired()) { 357 | $client->getCache()->clear(); 358 | if ($client->isUsingApplicationDefaultCredentials()) { 359 | $client->fetchAccessTokenWithAssertion(); 360 | } else { 361 | $refreshToken = $client->getRefreshToken(); 362 | if ($refreshToken) { 363 | $client->fetchAccessTokenWithRefreshToken($refreshToken); 364 | $this->service = new Drive($client); 365 | } 366 | } 367 | } 368 | } 369 | 370 | protected function cleanOptParameters($parameters) 371 | { 372 | $operations = ['files.copy', 'files.create', 'files.delete', 373 | 'files.trash', 'files.get', 'files.list', 'files.update', 374 | 'files.watch']; 375 | $clean = []; 376 | 377 | foreach ($operations as $operation) { 378 | $clean[$operation] = []; 379 | if (isset($parameters[$operation])) { 380 | $clean[$operation] = $parameters[$operation]; 381 | } 382 | } 383 | 384 | foreach ($parameters as $key => $value) { 385 | if (in_array($key, $operations)) { 386 | unset($parameters[$key]); 387 | } 388 | } 389 | 390 | foreach ($operations as $operation) { 391 | $clean[$operation] = array_merge_recursive($parameters, $clean[$operation]); 392 | } 393 | 394 | return $clean; 395 | } 396 | 397 | private function setPathPrefix($prefix) 398 | { 399 | $this->prefixer = new PathPrefixer($prefix); 400 | } 401 | 402 | /** 403 | * @throws FilesystemException 404 | */ 405 | public function fileExists(string $path): bool 406 | { 407 | try { 408 | $location = $this->prefixer->prefixPath($path); 409 | $this->toVirtualPath($location, true, true); 410 | return true; 411 | } catch (UnableToReadFile $e) { 412 | return false; 413 | } 414 | } 415 | 416 | /** 417 | * @throws FilesystemException 418 | */ 419 | public function directoryExists(string $path): bool 420 | { 421 | try { 422 | $location = $this->prefixer->prefixPath($path); 423 | $this->toVirtualPath($location, true, true); 424 | return true; 425 | } catch (UnableToReadFile $e) { 426 | return false; 427 | } 428 | } 429 | 430 | private function writeData(string $location, $contents, Config $config) 431 | { 432 | $updating = null; 433 | $path = $this->prefixer->prefixPath($location); 434 | if ($this->useDisplayPaths) { 435 | try { 436 | $virtual_path = $this->toVirtualPath($path, true, true); 437 | $updating = true; // destination exists 438 | } catch (UnableToReadFile $e) { 439 | $updating = false; 440 | [$parentDir, $fileName] = $this->splitPath($path, false); 441 | $virtual_path = $this->toSingleVirtualPath($parentDir, false, true, true, true); 442 | if ($virtual_path === '') { 443 | $virtual_path = $fileName; 444 | } else { 445 | $virtual_path .= '/'.$fileName; 446 | } 447 | } 448 | if ($updating && is_array($virtual_path)) { 449 | // multiple destinations with the same display path -> remove all but the first created & the first gets replaced 450 | if (count($virtual_path) > 1) { 451 | // delete all but first 452 | $this->delete_by_id( 453 | array_map( 454 | function ($p) { 455 | return $this->splitPath($p, false)[1]; 456 | }, 457 | array_slice($virtual_path, 1) 458 | ) 459 | ); 460 | } 461 | $virtual_path = $virtual_path[0]; 462 | } 463 | } else { 464 | $virtual_path = $path; 465 | } 466 | 467 | try { 468 | $result = $this->upload(/** @scrutinizer ignore-type */ $virtual_path, $contents, $config, $updating); 469 | } catch (Throwable $e) { 470 | // Unnecesary 471 | } 472 | if (!isset($result) || !$result) { 473 | throw UnableToWriteFile::atLocation($path, 'Not able to write the file'); 474 | } 475 | } 476 | 477 | /** 478 | * {@inheritdoc} 479 | */ 480 | public function write(string $path, string $resource, Config $config): void 481 | { 482 | $this->writeData($path, $resource, $config); 483 | } 484 | 485 | /** 486 | * {@inheritdoc} 487 | */ 488 | public function writeStream(string $path, $resource, Config $config): void 489 | { 490 | $this->writeData($path, $resource, $config); 491 | } 492 | 493 | /** 494 | * {@inheritdoc} 495 | */ 496 | public function copy(string $location, string $destination, Config $config): void 497 | { 498 | $this->refreshToken(); 499 | $path = $this->prefixer->prefixPath($location); 500 | $newpath = $this->prefixer->prefixPath($destination); 501 | if ($this->useDisplayPaths) { 502 | $srcId = $this->toVirtualPath($path, false, true); 503 | $newpathDir = self::dirname($newpath); 504 | if ($this->fileExists($newpathDir)) { 505 | $this->delete($newpath); 506 | } 507 | $toPath = $this->toSingleVirtualPath($newpathDir, false, false, true, true); 508 | if ($toPath === false) { 509 | throw UnableToCopyFile::fromLocationTo($path, $newpath); 510 | } 511 | if ($toPath === '') { 512 | $toPath = $this->root; 513 | } 514 | $newParentId = $toPath; 515 | $fileName = basename($newpath); 516 | } else { 517 | [, $srcId] = $this->splitPath($path); 518 | [$newParentId, $fileName] = $this->splitPath($newpath); 519 | } 520 | 521 | $file = new DriveFile(); 522 | $file->setName($fileName); 523 | $file->setParents([ 524 | $newParentId 525 | ]); 526 | 527 | $newFile = $this->service->files->copy(/** @scrutinizer ignore-type */ $srcId, $file, $this->applyDefaultParams([ 528 | 'fields' => self::FETCHFIELDS_GET 529 | ], 'files.copy')); 530 | 531 | if ($newFile instanceof DriveFile) { 532 | $id = $newFile->getId(); 533 | $this->cacheFileObjects[$id] = $newFile; 534 | $this->cacheObjects([$id => $newFile]); 535 | if (isset($this->cacheHasDirs[$srcId])) { 536 | $this->cacheHasDirs[$id] = $this->cacheHasDirs[$srcId]; 537 | } 538 | if ($this->useSinglePathTransaction && $this->useDisplayPaths) { 539 | $this->cachedPaths[trim($newpathDir.'/'.$fileName, '/')] = $id; 540 | } 541 | 542 | $srcFile = $this->cacheFileObjects[$srcId]; 543 | $visibility = $this->getRawVisibility($srcFile); 544 | 545 | if ($config->get('visibility') === Visibility::PUBLIC || $visibility === Visibility::PUBLIC) { 546 | $this->publish($id); 547 | } else { 548 | $this->unPublish($id); 549 | } 550 | $this->resetRequest([$id, $newParentId]); 551 | return; 552 | } 553 | 554 | throw UnableToCopyFile::fromLocationTo($path, $newpath); 555 | } 556 | 557 | /** 558 | * @throws UnableToMoveFile 559 | * @throws FilesystemException 560 | */ 561 | public function move(string $source, string $destination, Config $config): void 562 | { 563 | if (!$this->fileExists($source)) { 564 | throw UnableToMoveFile::fromLocationTo($source, $destination); 565 | } 566 | try { 567 | $this->refreshToken(); 568 | $path = $this->prefixer->prefixPath($source); 569 | $newpath = $this->prefixer->prefixPath($destination); 570 | if ($this->useDisplayPaths) { 571 | $srcId = $this->toVirtualPath($path, false, true); 572 | $newpathDir = self::dirname($newpath); 573 | if ($this->fileExists($newpathDir)) { 574 | $this->delete($newpath); 575 | } 576 | $toPath = $this->toSingleVirtualPath($newpathDir, false, false, true, true); 577 | if ($toPath === false) { 578 | throw UnableToMoveFile::fromLocationTo($path, $newpath); 579 | } 580 | if ($toPath === '') { 581 | $toPath = $this->root; 582 | } 583 | $newParentId = $toPath; 584 | $fileName = basename($newpath); 585 | } else { 586 | [, $srcId] = $this->splitPath($path); 587 | [$newParentId, $fileName] = $this->splitPath($newpath); 588 | } 589 | 590 | $params = []; 591 | $origenFile = $this->getFileObject($srcId); 592 | $parents = $origenFile->getParents(); 593 | if (!in_array($newParentId, $parents)) { 594 | $params = array_merge($params, [ 595 | 'addParents' => [$newParentId], 596 | 'removeParents' => $parents, 597 | ]); 598 | } 599 | 600 | if (!empty($params) || $fileName !== $origenFile->getName()) { 601 | $file = new DriveFile(); 602 | $file->setName($fileName); 603 | $this->service->files->update(/** @scrutinizer ignore-type */ $srcId, $file, $this->applyDefaultParams($params, 'files.update')); 604 | } 605 | 606 | $newFile = $this->service->files->get($srcId, ['fields' => self::FETCHFIELDS_GET]); 607 | if ($newFile instanceof DriveFile) { 608 | $id = $newFile->getId(); 609 | $this->cacheFileObjects[$id] = $newFile; 610 | $this->cacheObjects([$id => $newFile]); 611 | if (isset($this->cacheHasDirs[$srcId])) { 612 | $this->cacheHasDirs[$id] = $this->cacheHasDirs[$srcId]; 613 | } 614 | if ($this->useSinglePathTransaction && $this->useDisplayPaths) { 615 | $this->cachedPaths[trim($newpathDir.'/'.$fileName, '/')] = $id; 616 | } 617 | 618 | $srcFile = $this->cacheFileObjects[$srcId]; 619 | $visibility = $this->getRawVisibility($srcFile); 620 | 621 | if ($config->get('visibility') === Visibility::PUBLIC || $visibility === Visibility::PUBLIC) { 622 | $this->publish($id); 623 | } else { 624 | $this->unPublish($id); 625 | } 626 | $this->resetRequest([$id, $newParentId]); 627 | return; 628 | } 629 | } catch (Throwable $exception) { 630 | throw UnableToMoveFile::fromLocationTo($source, $destination, $exception); 631 | } 632 | } 633 | 634 | /** 635 | * Delete an array of google file ids 636 | * 637 | * @param string[]|string $ids 638 | * @return bool 639 | */ 640 | protected function delete_by_id($ids) 641 | { 642 | $this->refreshToken(); 643 | $deleted = false; 644 | if (!is_array($ids)) { 645 | $ids = [$ids]; 646 | } 647 | foreach ($ids as $id) { 648 | if ($id !== '' && ($file = $this->getFileObject($id))) { 649 | if ($file->getParents()) { 650 | if ($this->usePermanentDelete && $this->service->files->delete($id, $this->applyDefaultParams([], 'files.delete'))) { 651 | $this->uncacheId($id); 652 | $deleted = true; 653 | } else { 654 | if (!$this->usePermanentDelete) { 655 | $file = new DriveFile(); 656 | $file->setTrashed(true); 657 | if ($this->service->files->update($id, $file, $this->applyDefaultParams([], 'files.update'))) { 658 | $this->uncacheId($id); 659 | $deleted = true; 660 | } 661 | } 662 | } 663 | } 664 | } 665 | } 666 | return $deleted; 667 | } 668 | 669 | /** 670 | * {@inheritdoc} 671 | */ 672 | public function delete(string $location): void 673 | { 674 | if ($location === '' || $location === '/') { 675 | throw UnableToDeleteDirectory::atLocation($location, 'Unable to delete root'); 676 | } // do not allow deleting root... 677 | 678 | $path = $this->prefixer->prefixPath($location); 679 | $deleted = false; 680 | if ($this->useDisplayPaths) { 681 | try { 682 | $ids = $this->toVirtualPath($path, false); 683 | $deleted = $this->delete_by_id($ids); 684 | } catch (Throwable $exception) { 685 | $deleted = true; 686 | } 687 | } else { 688 | if ($file = $this->getFileObject($path)) { 689 | $deleted = $this->delete_by_id($file->getId()); 690 | } 691 | } 692 | 693 | if ($deleted) { 694 | $this->resetRequest('', true); 695 | } else { 696 | throw UnableToDeleteFile::atLocation($path, 'Unable to delete file'); 697 | } 698 | } 699 | 700 | /** 701 | * {@inheritdoc} 702 | */ 703 | public function deleteDirectory(string $dirname): void 704 | { 705 | try { 706 | $this->delete($dirname); 707 | } catch (Throwable $e) { 708 | throw UnableToDeleteDirectory::atLocation($dirname, 'Unable to delete directory'); 709 | } 710 | } 711 | 712 | /** 713 | * {@inheritdoc} 714 | */ 715 | public function createDirectory(string $dirname, Config $config): void 716 | { 717 | try { 718 | $meta = $this->getMetadata($dirname); 719 | } catch (UnableToReadFile $e) { 720 | $meta = false; 721 | } 722 | 723 | if ($meta !== false) { 724 | return; 725 | } 726 | 727 | [$pdir, $name] = $this->splitPath($dirname, false); 728 | if ($this->useDisplayPaths) { 729 | if ($pdir !== $this->root) { 730 | $pdir = $this->toSingleVirtualPath($pdir, false, false, true, true); // recursion! 731 | if ($pdir === false) { 732 | throw UnableToCreateDirectory::atLocation($dirname, 'Failed to create dir'); 733 | } 734 | } 735 | } 736 | 737 | $folder = $this->createDir($name, $pdir !== '' ? basename($pdir) : $pdir); 738 | if ($folder !== null) { 739 | $itemId = $folder->getId(); 740 | $this->cacheFileObjects[$itemId] = $folder; 741 | $this->cacheHasDirs[$itemId] = false; 742 | $this->cacheObjects([$itemId => $folder]); 743 | return; 744 | } 745 | 746 | throw UnableToCreateDirectory::atLocation($dirname, 'Failed to create dir'); 747 | } 748 | 749 | /** 750 | * {@inheritdoc} 751 | */ 752 | public function has($path): bool 753 | { 754 | if ($this->useDisplayPaths) { 755 | $this->toVirtualPath($path, false); 756 | } 757 | return ($this->getFileObject($path, true) instanceof DriveFile); 758 | } 759 | 760 | /** 761 | * {@inheritdoc} 762 | */ 763 | public function read(string $location): string 764 | { 765 | $this->refreshToken(); 766 | $path = $this->prefixer->prefixPath($location); 767 | if ($this->useDisplayPaths) { 768 | $fileId = $this->toVirtualPath($path, false, true); 769 | } else { 770 | [, $fileId] = $this->splitPath($path); 771 | } 772 | /** @var ResponseInterface $response */ 773 | if (($response = $this->service->files->get(/** @scrutinizer ignore-type */ $fileId, $this->applyDefaultParams(['alt' => 'media'], 'files.get')))) { 774 | return (string)$response->getBody(); 775 | } 776 | throw UnableToReadFile::fromLocation($path, 'Unable To Read File'); 777 | } 778 | 779 | /** 780 | * {@inheritdoc} 781 | */ 782 | public function readStream(string $location) 783 | { 784 | $this->refreshToken(); 785 | $path = $this->prefixer->prefixPath($location); 786 | if ($this->useDisplayPaths) { 787 | $path = $this->toVirtualPath($path, false, true); 788 | } 789 | 790 | $redirect = null; 791 | if (func_num_args() > 1) { 792 | $redirect = func_get_arg(1); 793 | } 794 | 795 | if (!$redirect) { 796 | $redirect = [ 797 | 'cnt' => 0, 798 | 'url' => '', 799 | 'token' => '', 800 | 'cookies' => [] 801 | ]; 802 | if (($file = $this->getFileObject(/** @scrutinizer ignore-type */ $path))) { 803 | if ($file->getMimeType() === self::DIRMIME) { 804 | throw UnableToReadFile::fromLocation($location, 'Unable To Read File'); 805 | } 806 | $dlurl = $this->getDownloadUrl($file); 807 | $client = $this->service->getClient(); 808 | /** @var array|string|object $token */ 809 | if ($client->isUsingApplicationDefaultCredentials()) { 810 | $token = $client->fetchAccessTokenWithAssertion(); 811 | } else { 812 | $token = $client->getAccessToken(); 813 | } 814 | $access_token = ''; 815 | if (is_array($token)) { 816 | if (empty($token['access_token']) && !empty($token['refresh_token'])) { 817 | $token = $client->fetchAccessTokenWithRefreshToken(); 818 | } 819 | $access_token = $token['access_token']; 820 | } else { 821 | if (($token = @json_decode($token))) { 822 | $access_token = $token->access_token; 823 | } 824 | } 825 | $redirect = [ 826 | 'cnt' => 0, 827 | 'url' => '', 828 | 'token' => $access_token, 829 | 'cookies' => [] 830 | ]; 831 | } 832 | } else { 833 | if ($redirect['cnt'] > 5) { 834 | throw UnableToReadFile::fromLocation($location, 'Unable To Read File'); 835 | } 836 | $dlurl = $redirect['url']; 837 | $redirect['url'] = ''; 838 | $access_token = $redirect['token']; 839 | } 840 | 841 | if (!empty($dlurl)) { 842 | $url = parse_url($dlurl); 843 | $cookies = []; 844 | if ($redirect['cookies']) { 845 | foreach ($redirect['cookies'] as $d => $c) { 846 | if (strpos($url['host'], $d) !== false) { 847 | $cookies[] = $c; 848 | } 849 | } 850 | } 851 | if (!empty($access_token)) { 852 | $query = isset($url['query']) ? '?'.$url['query'] : ''; 853 | $stream = stream_socket_client('ssl://'.$url['host'].':443'); 854 | stream_set_timeout($stream, 300); 855 | fwrite($stream, "GET {$url['path']}{$query} HTTP/1.1\r\n"); 856 | fwrite($stream, "Host: {$url['host']}\r\n"); 857 | fwrite($stream, "Authorization: Bearer {$access_token}\r\n"); 858 | fwrite($stream, "Connection: Close\r\n"); 859 | if ($cookies) { 860 | fwrite($stream, 'Cookie: '.implode('; ', $cookies)."\r\n"); 861 | } 862 | fwrite($stream, "\r\n"); 863 | while (($res = trim(fgets($stream))) !== '') { 864 | // find redirect 865 | if (preg_match('/^Location: (.+)$/', $res, $m)) { 866 | $redirect['url'] = $m[1]; 867 | } 868 | // fetch cookie 869 | if (strpos($res, 'Set-Cookie:') === 0) { 870 | $domain = $url['host']; 871 | if (preg_match('/^Set-Cookie:(.+)(?:domain=\s*([^ ;]+))?/i', $res, $c1)) { 872 | if (!empty($c1[2])) { 873 | $domain = trim($c1[2]); 874 | } 875 | if (preg_match('/([^ ]+=[^;]+)/', $c1[1], $c2)) { 876 | $redirect['cookies'][$domain] = $c2[1]; 877 | } 878 | } 879 | } 880 | } 881 | if ($redirect['url']) { 882 | $redirect['cnt']++; 883 | fclose($stream); 884 | return $this->readStream($path, $redirect); 885 | } 886 | return $stream; 887 | } 888 | } 889 | throw UnableToReadFile::fromLocation($location, 'Downloaded object does not contain a file resource.'); 890 | } 891 | 892 | /** 893 | * {@inheritdoc} 894 | */ 895 | public function listContents(string $directory, bool $recursive): iterable 896 | { 897 | $this->refreshToken(); 898 | $path = $this->prefixer->prefixPath($directory); 899 | if ($this->useDisplayPaths) { 900 | $time = microtime(true); 901 | $vp = $this->toVirtualPath($path ?: ''); 902 | $elapsed = (microtime(true) - $time) * 1000.0; 903 | if (!is_array($vp)) { 904 | $vp = [$vp]; 905 | } 906 | 907 | foreach ($vp as $path) { 908 | if (DEBUG_ME) { 909 | echo 'Converted display path to virtual path ['.number_format($elapsed, 1).'ms]: '.$path."\n"; 910 | } 911 | foreach (array_values($this->getItems($path, $recursive)) as $item) { 912 | yield $item; 913 | } 914 | } 915 | } else { 916 | foreach (array_values($this->getItems($path, $recursive)) as $item) { 917 | yield $item; 918 | } 919 | } 920 | } 921 | 922 | /** 923 | * Get metadata from file/dir 924 | * 925 | * @param string $path itemId path 926 | * @return 927 | */ 928 | public function getMetadata(string $path) 929 | { 930 | if ($this->useDisplayPaths) { 931 | $path = $this->toVirtualPath($path, true, true); 932 | } 933 | if (($obj = $this->getFileObject(/** @scrutinizer ignore-type */ $path, true))) { 934 | if ($obj instanceof DriveFile) { 935 | return $this->normaliseObject($obj, self::dirname($path)); 936 | } 937 | } 938 | return false; 939 | } 940 | 941 | private function fileAttributes(string $path, string $type = ''): FileAttributes 942 | { 943 | $exception = new Exception('Unable to get metadata'); 944 | $prefixedPath = $this->prefixer->prefixPath($path); 945 | 946 | try { 947 | $fileAttributes = $this->getMetadata($prefixedPath); 948 | } catch (Throwable $exception) { 949 | // Unnecesary 950 | } 951 | 952 | if (!isset($fileAttributes) || !$fileAttributes instanceof FileAttributes) { 953 | if (!$type) { 954 | throw UnableToRetrieveMetadata::create($path, '', '', $exception); 955 | } else { 956 | throw UnableToRetrieveMetadata::$type($path, '', $exception); 957 | } 958 | } 959 | if ($type && $fileAttributes[$type] === null) { 960 | throw UnableToRetrieveMetadata::{$type}($path, '', $exception); 961 | } 962 | return $fileAttributes; 963 | } 964 | 965 | /** 966 | * {@inheritdoc} 967 | */ 968 | public function fileSize(string $path): FileAttributes 969 | { 970 | return $this->fileAttributes($path, 'fileSize'); 971 | } 972 | 973 | /** 974 | * {@inheritdoc} 975 | */ 976 | public function mimeType(string $path): FileAttributes 977 | { 978 | return $this->fileAttributes($path, 'mimeType'); 979 | } 980 | 981 | /** 982 | * {@inheritdoc} 983 | */ 984 | public function lastModified(string $path): FileAttributes 985 | { 986 | return $this->fileAttributes($path, 'lastModified'); 987 | } 988 | 989 | /** 990 | * {@inheritdoc} 991 | */ 992 | public function setVisibility(string $path, string $visibility): void 993 | { 994 | try { 995 | if ($this->useDisplayPaths) { 996 | $path = $this->toVirtualPath($path, false, true); 997 | } 998 | $result = ($visibility === Visibility::PUBLIC) ? $this->publish(/** @scrutinizer ignore-type */ $path) : $this->unPublish(/** @scrutinizer ignore-type */ $path); 999 | } catch (Throwable $e) { 1000 | throw UnableToSetVisibility::atLocation(/** @scrutinizer ignore-type */ $path, 'Error setting visibility', $e); 1001 | } 1002 | if (!$result) { 1003 | $className = Visibility::class; 1004 | throw InvalidVisibilityProvided::withVisibility( 1005 | $visibility, 1006 | "either {$className}::PUBLIC or {$className}::PRIVATE" 1007 | ); 1008 | } 1009 | } 1010 | 1011 | /** 1012 | * {@inheritdoc} 1013 | */ 1014 | public function visibility(string $location): FileAttributes 1015 | { 1016 | $path = $this->prefixer->prefixPath($location); 1017 | try { 1018 | if ($this->useDisplayPaths) { 1019 | $path = $this->toVirtualPath($path, false, true); 1020 | } 1021 | $file = $this->getFileObject(/** @scrutinizer ignore-type */ $path); 1022 | } catch (Throwable $e) { 1023 | // Unnecesary 1024 | } 1025 | if (!isset($file) || !$file) { 1026 | throw UnableToRetrieveMetadata::visibility($location, '', new Exception('Error finding the file')); 1027 | } 1028 | 1029 | $visibility = $this->getRawVisibility($file); 1030 | 1031 | return new FileAttributes(/** @scrutinizer ignore-type */ $path, null, $visibility); 1032 | } 1033 | 1034 | // /////////////////- ORIGINAL METHODS -/////////////////// 1035 | 1036 | /** 1037 | * Get contents parmanent URL 1038 | * 1039 | * @param string $path itemId path 1040 | * @param string $path itemId path 1041 | */ 1042 | public function getUrl($path) 1043 | { 1044 | if ($this->useDisplayPaths) { 1045 | $path = $this->toVirtualPath($path, false, true); 1046 | } 1047 | if ($this->publish(/** @scrutinizer ignore-type */ $path)) { 1048 | $obj = $this->getFileObject(/** @scrutinizer ignore-type */ $path); 1049 | if (($url = $obj->getWebContentLink())) { 1050 | return str_replace('export=download', 'export=media', $url); 1051 | } 1052 | if (($url = $obj->getWebViewLink())) { 1053 | return $url; 1054 | } 1055 | if ($obj->mimeType === self::DIRMIME) { 1056 | return 'https://drive.google.com/drive/folders/'.$obj->id.'?usp=sharing'; 1057 | } 1058 | } 1059 | return ''; 1060 | } 1061 | 1062 | /** 1063 | * Has child directory 1064 | * 1065 | * @param string $path itemId path 1066 | * @return array 1067 | */ 1068 | public function hasDir($path) 1069 | { 1070 | $meta = $this->getMetadata($path)->extraMetadata(); 1071 | return (is_array($meta) && isset($meta['hasdir'])) ? $meta : [ 1072 | 'hasdir' => true 1073 | ]; 1074 | } 1075 | 1076 | /** 1077 | * Do cache cacheHasDirs with batch request 1078 | * 1079 | * @param array $targets [[path => id],...] 1080 | * @param array $object 1081 | * @return array 1082 | */ 1083 | protected function setHasDir($targets, $object) 1084 | { 1085 | $this->refreshToken(); 1086 | $service = $this->service; 1087 | $client = $service->getClient(); 1088 | $gFiles = $service->files; 1089 | 1090 | $opts = [ 1091 | 'pageSize' => 1, 1092 | 'orderBy' => 'folder,modifiedTime,name', 1093 | ]; 1094 | 1095 | $paths = []; 1096 | $client->setUseBatch(true); 1097 | $batch = $service->createBatch(); 1098 | $i = 0; 1099 | foreach ($targets as $id) { 1100 | $opts['q'] = sprintf('trashed = false and "%s" in parents and mimeType = "%s"', $id, self::DIRMIME); 1101 | /** @var RequestInterface $request */ 1102 | $request = $gFiles->listFiles($this->applyDefaultParams($opts, 'files.list')); 1103 | $key = ++$i; 1104 | $batch->add($request, (string)$key); 1105 | $paths['response-'.$key] = $id; 1106 | } 1107 | $results = $batch->execute(); 1108 | foreach ($results as $key => $result) { 1109 | if ($result instanceof FileList) { 1110 | $array = $object[$paths[$key]]->jsonSerialize(); 1111 | $array['extra_metadata']['hasdir'] = $this->cacheHasDirs[$paths[$key]] = (bool)$result->getFiles(); 1112 | $object[$paths[$key]] = DirectoryAttributes::fromArray($array); 1113 | } 1114 | } 1115 | $client->setUseBatch(false); 1116 | return $object; 1117 | } 1118 | 1119 | /** 1120 | * Get the object permissions presented as a visibility. 1121 | * 1122 | * @param string $path itemId path 1123 | * @return string 1124 | */ 1125 | private function getRawVisibility($file) 1126 | { 1127 | $permissions = $file->getPermissions(); 1128 | $visibility = Visibility::PRIVATE; 1129 | 1130 | if (empty($permissions)) { 1131 | $permissions = $this->service->permissions->listPermissions($file->getId(), $this->applyDefaultParams([], 'permissions.list')); 1132 | $file->setPermissions($permissions); 1133 | } 1134 | 1135 | foreach ($permissions as $permission) { 1136 | if ($permission->type === $this->publishPermission['type'] && $permission->role === $this->publishPermission['role']) { 1137 | $visibility = Visibility::PUBLIC; 1138 | break; 1139 | } 1140 | } 1141 | return $visibility; 1142 | } 1143 | 1144 | /** 1145 | * Publish specified path item 1146 | * 1147 | * @param string $path itemId path 1148 | * @return bool 1149 | */ 1150 | protected function publish($path) 1151 | { 1152 | $this->refreshToken(); 1153 | if (($file = $this->getFileObject($path))) { 1154 | if ($this->getRawVisibility($file) === Visibility::PUBLIC) { 1155 | return true; 1156 | } 1157 | try { 1158 | $new_permission = new Permission($this->publishPermission); 1159 | if ($permission = $this->service->permissions->create($file->getId(), $new_permission, $this->applyDefaultParams([], 'files.create'))) { 1160 | $file->setPermissions([$permission]); 1161 | return true; 1162 | } 1163 | } catch (Throwable $e) { 1164 | return false; 1165 | } 1166 | } 1167 | 1168 | return false; 1169 | } 1170 | 1171 | /** 1172 | * Un-publish specified path item 1173 | * 1174 | * @param string $path itemId path 1175 | * @return bool 1176 | */ 1177 | protected function unPublish($path) 1178 | { 1179 | $this->refreshToken(); 1180 | if (($file = $this->getFileObject($path))) { 1181 | if ($this->getRawVisibility($file) !== Visibility::PUBLIC) { 1182 | return true; 1183 | } 1184 | $permissions = $file->getPermissions(); 1185 | try { 1186 | foreach ($permissions as $permission) { 1187 | if ($permission->type === $this->publishPermission['type'] && $permission->role === $this->publishPermission['role'] && !empty($file->getId())) { 1188 | $this->service->permissions->delete($file->getId(), $permission->getId(), $this->applyDefaultParams([], 'files.trash')); 1189 | } 1190 | } 1191 | $file->setPermissions([]); 1192 | return true; 1193 | } catch (Throwable $e) { 1194 | return false; 1195 | } 1196 | } 1197 | 1198 | return false; 1199 | } 1200 | 1201 | /** 1202 | * Path splits to dirId, fileId or newName 1203 | * 1204 | * @param string $path 1205 | * @param bool $getParentId True => return only parent id, False => return full path (basically the same as dirname($path)) 1206 | * @return array [ $dirId , $fileId|newName ] 1207 | */ 1208 | protected function splitPath($path, $getParentId = true) 1209 | { 1210 | if ($path === '' || $path === '/') { 1211 | $fileName = $this->root; 1212 | $dirName = ''; 1213 | } else { 1214 | $paths = explode('/', $path); 1215 | $fileName = array_pop($paths); 1216 | if ($getParentId) { 1217 | $dirName = $paths ? array_pop($paths) : ''; 1218 | } else { 1219 | $dirName = implode('/', $paths); 1220 | } 1221 | if ($dirName === '') { 1222 | $dirName = $this->root; 1223 | } 1224 | } 1225 | return [ 1226 | $dirName, 1227 | $fileName 1228 | ]; 1229 | } 1230 | 1231 | /** 1232 | * Item name splits to filename and extension 1233 | * This function supported include '/' in item name 1234 | * 1235 | * @param string $name 1236 | * @return array [ 'filename' => $filename , 'extension' => $extension ] 1237 | */ 1238 | protected function splitFileExtension($name) 1239 | { 1240 | $name_parts = explode('.', $name); 1241 | $extension = isset($name_parts[1]) ? array_pop($name_parts) : ''; 1242 | $filename = implode('.', $name_parts); 1243 | return compact('filename', 'extension'); 1244 | } 1245 | 1246 | /** 1247 | * Get normalised files array from DriveFile 1248 | * 1249 | * @param DriveFile $object 1250 | * @param string $dirname Parent directory itemId path 1251 | * @return \League\Flysystem\StorageAttributes Normalised files array 1252 | */ 1253 | protected function normaliseObject(DriveFile $object, $dirname) 1254 | { 1255 | $id = $object->getId(); 1256 | $path_parts = $this->splitFileExtension($object->getName()); 1257 | if ($object->mimeType == self::SHORTCUTMIME) { 1258 | $object->mimeType = $object->shortcutDetails->targetMimeType; 1259 | $id = $object->shortcutDetails->targetId; 1260 | } 1261 | $type = $object->mimeType === self::DIRMIME ? 'dir' : 'file'; 1262 | $result = [ 1263 | 'id' => $id, 1264 | 'name' => $object->getName(), 1265 | ]; 1266 | $visibility = Visibility::PRIVATE; 1267 | $permissions = $object->getPermissions(); 1268 | try { 1269 | foreach ($permissions as $permission) { 1270 | if ($permission->type === $this->publishPermission['type'] && $permission->role === $this->publishPermission['role']) { 1271 | $visibility = Visibility::PUBLIC; 1272 | break; 1273 | } 1274 | } 1275 | } catch (Throwable $e) { 1276 | // Unnecesary 1277 | } 1278 | 1279 | $result['virtual_path'] = ($dirname ? ($dirname.'/') : '').$id; 1280 | $result['display_path'] = $this->useDisplayPaths || $this->showDisplayPaths ? $this->toDisplayPath($result['virtual_path']) : $result['virtual_path']; 1281 | 1282 | if ($type === 'file') { 1283 | $result['filename'] = $path_parts['filename']; 1284 | $result['extension'] = $path_parts['extension']; 1285 | return new FileAttributes( 1286 | $this->useDisplayPaths ? $result['display_path'] : $result['virtual_path'], 1287 | (int)$object->getSize(), 1288 | $visibility, 1289 | strtotime($object->getModifiedTime()), 1290 | $object->mimeType, 1291 | $result); 1292 | } 1293 | if ($type === 'dir') { 1294 | if ($this->useHasDir) { 1295 | $result['hasdir'] = isset($this->cacheHasDirs[$id]) ? $this->cacheHasDirs[$id] : false; 1296 | } 1297 | $result['dirname'] = $path_parts['filename']; 1298 | return new DirectoryAttributes( 1299 | rtrim($this->useDisplayPaths ? $result['display_path'] : $result['virtual_path'], '/'), 1300 | $visibility, 1301 | strtotime($object->getModifiedTime()), 1302 | $result); 1303 | } 1304 | } 1305 | 1306 | /** 1307 | * Get items array of target directory 1308 | * 1309 | * @param string $dirname itemId path 1310 | * @param bool $recursive 1311 | * @param int $maxResults 1312 | * @param string $query 1313 | * @return array Items array 1314 | */ 1315 | protected function getItems($dirname, $recursive = false, $maxResults = 0, $query = '') 1316 | { 1317 | $this->refreshToken(); 1318 | [, $itemId] = $this->splitPath($dirname); 1319 | 1320 | $maxResults = min($maxResults, 1000); 1321 | $results = []; 1322 | $parameters = [ 1323 | 'pageSize' => $maxResults ?: 1000, 1324 | 'fields' => self::FETCHFIELDS_LIST, 1325 | 'orderBy' => 'folder,modifiedTime,name', 1326 | 'spaces' => $this->spaces, 1327 | 'q' => sprintf('trashed = false and "%s" in parents', $itemId) 1328 | ]; 1329 | if ($query) { 1330 | $parameters['q'] .= ' and ('.$query.')'; 1331 | } 1332 | $pageToken = null; 1333 | $gFiles = $this->service->files; 1334 | $this->cacheHasDirs[$itemId] = false; 1335 | $setHasDir = []; 1336 | 1337 | do { 1338 | try { 1339 | if ($pageToken) { 1340 | $parameters['pageToken'] = $pageToken; 1341 | } 1342 | $fileObjs = $gFiles->listFiles($this->applyDefaultParams($parameters, 'files.list')); 1343 | if ($fileObjs instanceof FileList) { 1344 | foreach ($fileObjs as $obj) { 1345 | $id = $obj->getId(); 1346 | $this->cacheFileObjects[$id] = $obj; 1347 | $result = $this->normaliseObject($obj, $dirname); 1348 | $results[$id] = $result; 1349 | if ($result->isDir()) { 1350 | if ($this->useHasDir) { 1351 | $setHasDir[$id] = $id; 1352 | } 1353 | if ($this->cacheHasDirs[$itemId] === false) { 1354 | $this->cacheHasDirs[$itemId] = true; 1355 | unset($setHasDir[$itemId]); 1356 | } 1357 | if ($recursive) { 1358 | $results = array_merge($results, $this->getItems($result->extraMetadata()['virtual_path'], true, $maxResults, $query)); 1359 | } 1360 | } 1361 | } 1362 | $pageToken = $fileObjs->getNextPageToken(); 1363 | } else { 1364 | $pageToken = null; 1365 | } 1366 | } catch (Throwable $e) { 1367 | $pageToken = null; 1368 | } 1369 | } while ($pageToken && $maxResults === 0); 1370 | 1371 | if ($setHasDir) { 1372 | $results = $this->setHasDir($setHasDir, $results); 1373 | } 1374 | return array_values($results); 1375 | } 1376 | 1377 | /** 1378 | * Get file oblect DriveFile 1379 | * 1380 | * @param string $path itemId path 1381 | * @param bool $checkDir do check hasdir 1382 | * @return DriveFile|null 1383 | */ 1384 | public function getFileObject($path, $checkDir = false) 1385 | { 1386 | [, $itemId] = $this->splitPath($path); 1387 | if (isset($this->cacheFileObjects[$itemId])) { 1388 | return $this->cacheFileObjects[$itemId]; 1389 | } 1390 | $this->refreshToken(); 1391 | $service = $this->service; 1392 | $client = $service->getClient(); 1393 | 1394 | $client->setUseBatch(true); 1395 | try { 1396 | $batch = $service->createBatch(); 1397 | 1398 | $opts = [ 1399 | 'fields' => self::FETCHFIELDS_GET 1400 | ]; 1401 | 1402 | /** @var RequestInterface $request */ 1403 | $request = $this->service->files->get($itemId, $opts); 1404 | $batch->add($request, 'obj'); 1405 | 1406 | if ($checkDir && $this->useHasDir) { 1407 | /** @var RequestInterface $request */ 1408 | $request = $service->files->listFiles($this->applyDefaultParams([ 1409 | 'pageSize' => 1, 1410 | 'orderBy' => 'folder,modifiedTime,name', 1411 | 'q' => sprintf('trashed = false and "%s" in parents and mimeType = "%s"', $itemId, self::DIRMIME) 1412 | ], 'files.list')); 1413 | 1414 | $batch->add($request, 'hasdir'); 1415 | } 1416 | $results = array_values($batch->execute() ?: []); 1417 | 1418 | [$fileObj, $hasdir] = array_pad($results, 2, null); 1419 | } finally { 1420 | $client->setUseBatch(false); 1421 | } 1422 | 1423 | if ($fileObj instanceof DriveFile) { 1424 | if ($hasdir && $fileObj->mimeType === self::DIRMIME) { 1425 | if ($hasdir instanceof FileList) { 1426 | $this->cacheHasDirs[$fileObj->getId()] = (bool)$hasdir->getFiles(); 1427 | } 1428 | } 1429 | } else { 1430 | $fileObj = null; 1431 | } 1432 | 1433 | if ($fileObj !== null) { 1434 | $this->cacheFileObjects[$itemId] = $fileObj; 1435 | $this->cacheObjects([$itemId => $fileObj]); 1436 | } 1437 | 1438 | return $fileObj; 1439 | } 1440 | 1441 | /** 1442 | * Get download url 1443 | * 1444 | * @param DriveFile $file 1445 | * @return string|false 1446 | */ 1447 | protected function getDownloadUrl($file) 1448 | { 1449 | if (strpos($file->mimeType, 'application/vnd.google-apps') !== 0) { 1450 | $params = $this->applyDefaultParams(['alt' => 'media'], 'files.get'); 1451 | foreach ($params as $key => $value) { 1452 | if (is_bool($value)) { 1453 | $params[$key] = $value ? 'true' : 'false'; 1454 | } 1455 | } 1456 | return 'https://www.googleapis.com/drive/v3/files/'.$file->getId().'?'.http_build_query($params); 1457 | } 1458 | 1459 | $mimeMap = $this->options['appsExportMap']; 1460 | if (isset($mimeMap[$file->getMimeType()])) { 1461 | $mime = $mimeMap[$file->getMimeType()]; 1462 | } else { 1463 | $mime = $mimeMap['default']; 1464 | } 1465 | 1466 | $params = $this->applyDefaultParams(['mimeType' => $mime], 'files.get'); 1467 | return 'https://www.googleapis.com/drive/v3/files/'.$file->getId().'/export?'.http_build_query($params); 1468 | } 1469 | 1470 | /** 1471 | * Create directory 1472 | * 1473 | * @param string $name 1474 | * @param string $parentId 1475 | * @return DriveFile|null 1476 | */ 1477 | protected function createDir($name, $parentId) 1478 | { 1479 | $this->refreshToken(); 1480 | $file = new DriveFile(); 1481 | $file->setName($name); 1482 | $file->setParents([ 1483 | $parentId 1484 | ]); 1485 | $file->setMimeType(self::DIRMIME); 1486 | 1487 | $obj = $this->service->files->create($file, $this->applyDefaultParams([ 1488 | 'fields' => self::FETCHFIELDS_GET 1489 | ], 'files.create')); 1490 | $this->resetRequest($parentId); 1491 | 1492 | return ($obj instanceof DriveFile) ? $obj : null; 1493 | } 1494 | 1495 | /** 1496 | * Upload|Update item 1497 | * 1498 | * @param string $path 1499 | * @param string|resource $contents 1500 | * @param Config $config 1501 | * @param bool|null $updating If null then we check for existence of the file 1502 | * @return \League\Flysystem\StorageAttributes|false item info 1503 | */ 1504 | protected function upload($path, $contents, Config $config, $updating = null) 1505 | { 1506 | $this->refreshToken(); 1507 | [$parentId, $fileName] = $this->splitPath($path); 1508 | $mime = $config->get('mimetype'); 1509 | $file = new DriveFile(); 1510 | 1511 | if ($updating === null || $updating === true) { 1512 | $srcFile = $this->getFileObject($path); 1513 | $updating = $srcFile !== null; 1514 | } else { 1515 | $srcFile = null; 1516 | } 1517 | if (!$updating) { 1518 | $file->setName($fileName); 1519 | $file->setParents([ 1520 | $parentId 1521 | ]); 1522 | } 1523 | 1524 | if (!$mime) { 1525 | $mime = self::guessMimeType($fileName, is_string($contents) ? $contents : ''); 1526 | if (empty($mime)) { 1527 | $mime = 'application/octet-stream'; 1528 | } 1529 | } 1530 | $file->setMimeType($mime); 1531 | 1532 | /** @var StreamInterface $stream */ 1533 | $stream = Utils::streamFor($contents); 1534 | $size = $stream->getSize(); 1535 | 1536 | if ($size <= self::MAX_CHUNK_SIZE) { 1537 | // one shot upload 1538 | $params = [ 1539 | 'data' => $stream, 1540 | 'uploadType' => 'media', 1541 | 'fields' => self::FETCHFIELDS_GET 1542 | ]; 1543 | 1544 | if (!$updating) { 1545 | $obj = $this->service->files->create($file, $this->applyDefaultParams($params, 'files.create')); 1546 | } else { 1547 | $obj = $this->service->files->update($srcFile->getId(), $file, $this->applyDefaultParams($params, 'files.update')); 1548 | } 1549 | } else { 1550 | // chunked upload 1551 | $client = $this->service->getClient(); 1552 | 1553 | $params = [ 1554 | 'fields' => self::FETCHFIELDS_GET 1555 | ]; 1556 | 1557 | $client->setDefer(true); 1558 | if (!$updating) { 1559 | /** @var RequestInterface $request */ 1560 | $request = $this->service->files->create($file, $this->applyDefaultParams($params, 'files.create')); 1561 | } else { 1562 | /** @var RequestInterface $request */ 1563 | $request = $this->service->files->update($srcFile->getId(), $file, $this->applyDefaultParams($params, 'files.update')); 1564 | } 1565 | 1566 | $media = new StreamableUpload($client, $request, $mime, $stream, true, self::MAX_CHUNK_SIZE); 1567 | $media->setFileSize($size); 1568 | do { 1569 | if (DEBUG_ME) { 1570 | echo "* Uploading next chunk.\n"; 1571 | } 1572 | $status = $media->nextChunk(); 1573 | } while ($status === false); 1574 | 1575 | // The final value of $status will be the data from the API for the object that has been uploaded. 1576 | if ($status !== false) { 1577 | $obj = $status; 1578 | } 1579 | 1580 | $client->setDefer(false); 1581 | } 1582 | 1583 | $this->resetRequest($parentId); 1584 | 1585 | if (isset($obj) && $obj instanceof DriveFile) { 1586 | $this->cacheFileObjects[$obj->getId()] = $obj; 1587 | $this->cacheObjects([$obj->getId() => $obj]); 1588 | $result = $this->normaliseObject($obj, self::dirname($path)); 1589 | if ($this->useSinglePathTransaction && $this->useDisplayPaths) { 1590 | $this->cachedPaths[$result->extraMetadata()['display_path']] = $obj->getId(); 1591 | } 1592 | 1593 | if ($config->get('visibility') === Visibility::PUBLIC) { 1594 | $this->publish($obj->getId()); 1595 | } else { 1596 | $this->unpublish($obj->getId()); 1597 | } 1598 | return $result; 1599 | } 1600 | return false; 1601 | } 1602 | 1603 | /** 1604 | * @param array $ids 1605 | * @param bool $checkDir 1606 | * @return array 1607 | */ 1608 | protected function getObjects($ids, $checkDir = false) 1609 | { 1610 | if ($checkDir && !$this->useHasDir) { 1611 | $checkDir = false; 1612 | } 1613 | 1614 | $fetch = []; 1615 | foreach ($ids as $itemId) { 1616 | if (!isset($this->cacheFileObjects[$itemId])) { 1617 | $fetch[$itemId] = null; 1618 | } 1619 | } 1620 | if (!empty($fetch) || $checkDir) { 1621 | $this->refreshToken(); 1622 | $service = $this->service; 1623 | $client = $service->getClient(); 1624 | 1625 | $client->setUseBatch(true); 1626 | try { 1627 | $batch = $service->createBatch(); 1628 | 1629 | $opts = [ 1630 | 'fields' => self::FETCHFIELDS_GET 1631 | ]; 1632 | 1633 | $count = 0; 1634 | if (!$this->rootId) { 1635 | /** @var RequestInterface $request */ 1636 | $request = $this->service->files->get($this->root, $this->applyDefaultParams($opts, 'files.get')); 1637 | $batch->add($request, 'rootdir'); 1638 | $count++; 1639 | } 1640 | 1641 | $results = []; 1642 | foreach ($fetch as $itemId => $value) { 1643 | if (DEBUG_ME) { 1644 | echo "*** FETCH *** $itemId\n"; 1645 | } 1646 | 1647 | /** @var RequestInterface $request */ 1648 | $request = $this->service->files->get($itemId, $opts); 1649 | $batch->add($request, $itemId); 1650 | $count++; 1651 | 1652 | if ($checkDir) { 1653 | /** @var RequestInterface $request */ 1654 | $request = $service->files->listFiles($this->applyDefaultParams([ 1655 | 'pageSize' => 1, 1656 | 'orderBy' => 'folder,modifiedTime,name', 1657 | 'q' => sprintf('trashed = false and "%s" in parents and mimeType = "%s"', $itemId, self::DIRMIME) 1658 | ], 'files.list')); 1659 | $batch->add($request, 'hasdir-'.$itemId); 1660 | $count++; 1661 | } 1662 | 1663 | if ($count > 90) { 1664 | // batch requests are limited to 100 calls in a single batch request 1665 | $results[] = $batch->execute(); 1666 | $batch = $service->createBatch(); 1667 | $count = 0; 1668 | } 1669 | } 1670 | if ($count > 0) { 1671 | $results[] = $batch->execute(); 1672 | } 1673 | if (!empty($results)) { 1674 | $results = array_merge(...$results); 1675 | } 1676 | 1677 | foreach ($results as $key => $value) { 1678 | if ($value instanceof DriveFile) { 1679 | $itemId = $value->getId(); 1680 | $this->cacheFileObjects[$itemId] = $value; 1681 | if (!$this->rootId && strcmp($key, 'response-rootdir') === 0) { 1682 | $this->rootId = $itemId; 1683 | } 1684 | } else { 1685 | if ($checkDir && $value instanceof FileList) { 1686 | if (strncmp($key, 'response-hasdir-', 16) === 0) { 1687 | $key = substr($key, 16); 1688 | if (isset($this->cacheFileObjects[$key]) && $this->cacheFileObjects[$key]->mimeType === self::DIRMIME) { 1689 | $this->cacheHasDirs[$key] = (bool)$value->getFiles(); 1690 | } 1691 | } 1692 | } 1693 | } 1694 | } 1695 | 1696 | $this->cacheObjects($results); 1697 | } finally { 1698 | $client->setUseBatch(false); 1699 | } 1700 | } 1701 | 1702 | $objects = []; 1703 | foreach ($ids as $itemId) { 1704 | $objects[$itemId] = isset($this->cacheFileObjects[$itemId]) ? $this->cacheFileObjects[$itemId] : null; 1705 | } 1706 | return $objects; 1707 | } 1708 | 1709 | protected function buildPathFromCacheFileObjects($lastItemId) 1710 | { 1711 | $complete_paths = []; 1712 | $itemIds = [$lastItemId]; 1713 | $paths = ['' => '']; 1714 | $is_first = true; 1715 | while (!empty($itemIds)) { 1716 | $new_itemIds = []; 1717 | $new_paths = []; 1718 | foreach ($itemIds as $itemId) { 1719 | if (empty($this->cacheFileObjects[$itemId])) { 1720 | continue; 1721 | } 1722 | 1723 | /* @var DriveFile $obj */ 1724 | $obj = $this->cacheFileObjects[$itemId]; 1725 | $parents = $obj->getParents(); 1726 | 1727 | foreach ($paths as $id => $path) { 1728 | if ($is_first) { 1729 | $is_first = false; 1730 | $new_path = $this->sanitizeFilename($obj->getName()); 1731 | $id = $itemId; 1732 | } else { 1733 | $new_path = $this->sanitizeFilename($obj->getName()).'/'.$path; 1734 | } 1735 | 1736 | if ($this->rootId === $itemId) { 1737 | if (!empty($path)) { 1738 | $complete_paths[$id] = $path; 1739 | } // this path is complete...don't include drive name 1740 | } else { 1741 | if (!empty($parents)) { 1742 | $new_paths[$id] = $new_path; 1743 | } 1744 | } 1745 | } 1746 | 1747 | if (!empty($parents)) { 1748 | $new_itemIds[] = (array)($obj->getParents()); 1749 | } 1750 | } 1751 | $paths = $new_paths; 1752 | $itemIds = !empty($new_itemIds) ? array_merge(...$new_itemIds) : []; 1753 | } 1754 | return $complete_paths; 1755 | } 1756 | 1757 | public function uncacheFolder($path) 1758 | { 1759 | if ($this->useDisplayPaths) { 1760 | try { 1761 | $path_id = $this->getCachedPathId($path); 1762 | if (is_array($path_id) && !empty($path_id[0] ?? null)) { 1763 | $this->uncacheId($path_id[0]); 1764 | } 1765 | } catch (UnableToReadFile $e) { 1766 | // unnecesary 1767 | } 1768 | } else { 1769 | $this->uncacheId($path); 1770 | } 1771 | } 1772 | 1773 | protected function uncacheId($id) 1774 | { 1775 | if (empty($id)) { 1776 | return; 1777 | } 1778 | $basePath = null; 1779 | foreach ($this->cachedPaths as $path => $itemId) { 1780 | if ($itemId === $id) { 1781 | $basePath = (string)$path; 1782 | break; 1783 | } 1784 | } 1785 | if ($basePath) { 1786 | foreach ($this->cachedPaths as $path => $itemId) { 1787 | if (strlen((string)$path) >= strlen($basePath) && strncmp((string)$path, $basePath, strlen($basePath)) === 0) { 1788 | unset($this->cachedPaths[$path]); 1789 | } 1790 | } 1791 | } 1792 | 1793 | unset($this->cacheFileObjects[$id], $this->cacheHasDirs[$id]); 1794 | } 1795 | 1796 | protected function cacheObjects($objects) 1797 | { 1798 | foreach ($objects as $key => $value) { 1799 | if ($value instanceof DriveFile) { 1800 | $complete_paths = $this->buildPathFromCacheFileObjects($value->getId()); 1801 | foreach ($complete_paths as $itemId => $path) { 1802 | if (DEBUG_ME) { 1803 | echo 'Complete path: '.$path.' ['.$itemId."]\n"; 1804 | } 1805 | 1806 | if (!isset($this->cachedPaths[$path])) { 1807 | $this->cachedPaths[$path] = $itemId; 1808 | } else { 1809 | if (!is_array($this->cachedPaths[$path])) { 1810 | if ($itemId !== $this->cachedPaths[$path]) { 1811 | // convert to array 1812 | $this->cachedPaths[$path] = [ 1813 | $this->cachedPaths[$path], 1814 | $itemId 1815 | ]; 1816 | 1817 | if (DEBUG_ME) { 1818 | echo 'Caching [DUP]: '.$path.' => '.$itemId."\n"; 1819 | } 1820 | } 1821 | } else { 1822 | if (!in_array($itemId, $this->cachedPaths[$path])) { 1823 | array_push($this->cachedPaths[$path], $itemId); 1824 | if (DEBUG_ME) { 1825 | echo 'Caching [DUP]: '.$path.' => '.$itemId."\n"; 1826 | } 1827 | } 1828 | } 1829 | } 1830 | } 1831 | } 1832 | } 1833 | } 1834 | 1835 | protected function indexString($str, $ch = '/') 1836 | { 1837 | $indices = []; 1838 | for ($i = 0, $len = strlen($str); $i < $len; $i++) { 1839 | if ($str[$i] === $ch) { 1840 | $indices[] = $i; 1841 | } 1842 | } 1843 | return $indices; 1844 | } 1845 | 1846 | protected function getCachedPathId($path, $indices = null) 1847 | { 1848 | $pathLen = strlen($path); 1849 | if ($indices === null) { 1850 | $indices = $this->indexString($path, '/'); 1851 | $indices[] = $pathLen; 1852 | } 1853 | 1854 | $maxLen = 0; 1855 | $itemId = null; 1856 | $pathMatch = null; 1857 | 1858 | foreach ($this->cachedPaths as $pathFrag => $id) { 1859 | $pathFrag = (string)$pathFrag; 1860 | $len = strlen($pathFrag); 1861 | if ($len > $pathLen || $len < $maxLen || !in_array($len, $indices)) { 1862 | continue; 1863 | } 1864 | 1865 | if (strncmp($pathFrag, $path, $len) === 0) { 1866 | if ($len === $pathLen) { 1867 | return [$id, $pathFrag]; 1868 | } // we found a perfect match 1869 | 1870 | $maxLen = $len; 1871 | $itemId = $id; 1872 | $pathMatch = $pathFrag; 1873 | } 1874 | } 1875 | 1876 | // we found a partial match or none at all 1877 | return [$itemId, $pathMatch]; 1878 | } 1879 | 1880 | protected function getPathToIndex($path, $i, $indices) 1881 | { 1882 | if ($i < 0) { 1883 | return ''; 1884 | } 1885 | if (!isset($indices[$i]) || !isset($indices[$i + 1])) { 1886 | return $path; 1887 | } 1888 | return substr($path, 0, $indices[$i]); 1889 | } 1890 | 1891 | protected function getToken($path, $i, $indices) 1892 | { 1893 | if ($i < 0 || !isset($indices[$i])) { 1894 | return ''; 1895 | } 1896 | $start = $i > 0 ? $indices[$i - 1] + 1 : 0; 1897 | return substr($path, $start, isset($indices[$i]) ? $indices[$i] - $start : null); 1898 | } 1899 | 1900 | protected function cachePaths($displayPath, $i, $indices, $parentItemId) 1901 | { 1902 | $nextItemId = $parentItemId; 1903 | for ($count = count($indices); $i < $indices; $i++) { 1904 | $token = $this->getToken($displayPath, $i, $indices); 1905 | if (empty($token) && $token !== '0') { 1906 | return; 1907 | } 1908 | $basePath = $this->getPathToIndex($displayPath, $i - 2, $indices); 1909 | if (!empty($basePath)) { 1910 | $basePath .= '/'; 1911 | } 1912 | 1913 | if ($nextItemId === null) { 1914 | return; 1915 | } 1916 | 1917 | $is_last = $i === $count - 1; 1918 | 1919 | // search only for directories unless it's the last token 1920 | if (!is_array($nextItemId)) { 1921 | $nextItemId = [$nextItemId]; 1922 | } 1923 | 1924 | $items = []; 1925 | foreach ($nextItemId as $id) { 1926 | if (!$this->canRequest($id, $is_last)) { 1927 | continue; 1928 | } 1929 | $this->markRequest($id, $is_last); 1930 | if (DEBUG_ME) { 1931 | echo 'New req: '.$id; 1932 | } 1933 | 1934 | $query = $is_last ? [] : ['mimeType = "'.self::DIRMIME.'"']; 1935 | if ($this->useSinglePathTransaction) { 1936 | $query[] = "name = '{$token}'"; 1937 | } 1938 | $items[] = $this->getItems($id, false, 0, implode(' and ', $query)); 1939 | if (DEBUG_ME) { 1940 | echo " ...done\n"; 1941 | } 1942 | } 1943 | if (!empty($items)) { 1944 | /** @noinspection SlowArrayOperationsInLoopInspection */ 1945 | $items = array_merge(...$items); 1946 | } 1947 | 1948 | $nextItemId = null; 1949 | foreach ($items as $itemObj) { 1950 | $item = $itemObj->extraMetadata(); 1951 | $itemId = basename($item['virtual_path']); 1952 | $fullPath = $basePath.$item['display_path']; 1953 | 1954 | // update cache 1955 | if (!isset($this->cachedPaths[$fullPath])) { 1956 | $this->cachedPaths[$fullPath] = $itemId; 1957 | if (DEBUG_ME) { 1958 | echo 'Caching: '.$fullPath.' => '.$itemId."\n"; 1959 | } 1960 | } else { 1961 | if (!is_array($this->cachedPaths[$fullPath])) { 1962 | if ($itemId !== $this->cachedPaths[$fullPath]) { 1963 | // convert to array 1964 | $this->cachedPaths[$fullPath] = [ 1965 | $this->cachedPaths[$fullPath], 1966 | $itemId 1967 | ]; 1968 | 1969 | if (DEBUG_ME) { 1970 | echo 'Caching [DUP]: '.$fullPath.' => '.$itemId."\n"; 1971 | } 1972 | } 1973 | } else { 1974 | if (!in_array($itemId, $this->cachedPaths[$fullPath])) { 1975 | $this->cachedPaths[$fullPath][] = $itemId; 1976 | if (DEBUG_ME) { 1977 | echo 'Caching [DUP]: '.$fullPath.' => '.$itemId."\n"; 1978 | } 1979 | } 1980 | } 1981 | } 1982 | 1983 | if (basename($item['display_path']) === $token) { 1984 | $nextItemId = $this->cachedPaths[$fullPath]; 1985 | } // found our token 1986 | } 1987 | } 1988 | } 1989 | 1990 | /** 1991 | * Create a full virtual path from cache 1992 | * 1993 | * @param string $displayPath 1994 | * @param bool $returnFirstItem return first item only 1995 | * @return string[]|string 1996 | * 1997 | * @throws UnableToReadFile 1998 | */ 1999 | protected function makeFullVirtualPath($displayPath, $returnFirstItem = false) 2000 | { 2001 | $paths = ['' => null]; 2002 | 2003 | $tmp = ''; 2004 | $tokens = explode('/', trim($displayPath, '/')); 2005 | foreach ($tokens as $token) { 2006 | if (empty($tmp)) { 2007 | $tmp .= $token; 2008 | } else { 2009 | $tmp .= '/'.$token; 2010 | } 2011 | 2012 | if (empty($this->cachedPaths[$tmp])) { 2013 | throw UnableToReadFile::fromLocation($displayPath, 'File not found'); 2014 | } 2015 | if (is_array($this->cachedPaths[$tmp])) { 2016 | $new_paths = []; 2017 | foreach ($paths as $path => $obj) { 2018 | $parentId = $path === '' ? '' : basename($path); 2019 | foreach ($this->cachedPaths[$tmp] as $id) { 2020 | if ($parentId === '' || (!empty($this->cacheFileObjects[$id]->parents) && in_array($parentId, $this->cacheFileObjects[$id]->parents))) { 2021 | $new_paths[$path.'/'.$id] = $this->cacheFileObjects[$id]; 2022 | } 2023 | } 2024 | } 2025 | $paths = $new_paths; 2026 | } else { 2027 | $id = $this->cachedPaths[$tmp]; 2028 | $new_paths = []; 2029 | foreach ($paths as $path => $obj) { 2030 | $parentId = $path === '' ? '' : basename($path); 2031 | if ($parentId === '' || (!empty($this->cacheFileObjects[$id]->parents) && in_array($parentId, $this->cacheFileObjects[$id]->parents))) { 2032 | $new_paths[$path.'/'.$id] = $this->cacheFileObjects[$id]; 2033 | } 2034 | } 2035 | $paths = $new_paths; 2036 | } 2037 | } 2038 | 2039 | $count = count($paths); 2040 | if ($count === 0) { 2041 | throw UnableToReadFile::fromLocation($displayPath, 'File not found'); 2042 | } 2043 | 2044 | if (count($paths) > 1) { 2045 | // sort oldest to newest 2046 | uasort($paths, function ($a, $b) { 2047 | $t1 = strtotime($a->getCreatedTime()); 2048 | $t2 = strtotime($b->getCreatedTime()); 2049 | if ($t1 < $t2) { 2050 | return -1; 2051 | } 2052 | if ($t1 > $t2) { 2053 | return 1; 2054 | } 2055 | return 0; 2056 | }); 2057 | 2058 | if (!$returnFirstItem) { 2059 | return array_keys($paths); 2060 | } 2061 | } 2062 | return array_keys($paths)[0]; 2063 | } 2064 | 2065 | protected function returnSingle($item, $returnFirstItem) 2066 | { 2067 | if ($returnFirstItem && is_array($item)) { 2068 | return $item[0]; 2069 | } 2070 | return $item; 2071 | } 2072 | 2073 | /** 2074 | * Convert display path to virtual path or just id 2075 | * 2076 | * @param string $displayPath 2077 | * @param bool $makeFullVirtualPath 2078 | * @param bool $returnFirstItem 2079 | * @return string[]|string Single itemId/path or array of them 2080 | * 2081 | * @throws UnableToReadFile 2082 | */ 2083 | protected function toVirtualPath($displayPath, $makeFullVirtualPath = true, $returnFirstItem = false) 2084 | { 2085 | if ($displayPath === '' || $displayPath === '/' || $displayPath === $this->root) { 2086 | return ''; 2087 | } 2088 | 2089 | $displayPath = trim($displayPath, '/'); // not needed 2090 | 2091 | $indices = $this->indexString($displayPath, '/'); 2092 | $indices[] = strlen($displayPath); 2093 | 2094 | [$itemId, $pathMatch] = $this->getCachedPathId($displayPath, $indices); 2095 | $i = 0; 2096 | if ($pathMatch !== null) { 2097 | if (strcmp($pathMatch, $displayPath) === 0) { 2098 | if ($makeFullVirtualPath) { 2099 | return $this->makeFullVirtualPath($displayPath, $returnFirstItem); 2100 | } 2101 | return $this->returnSingle($itemId, $returnFirstItem); 2102 | } 2103 | $i = array_search(strlen($pathMatch), $indices) + 1; 2104 | } 2105 | if ($itemId === null) { 2106 | $itemId = ''; 2107 | } 2108 | $this->cachePaths($displayPath, $i, $indices, $itemId); 2109 | 2110 | if ($makeFullVirtualPath) { 2111 | return $this->makeFullVirtualPath($displayPath, $returnFirstItem); 2112 | } 2113 | 2114 | if (empty($this->cachedPaths[$displayPath])) { 2115 | throw UnableToReadFile::fromLocation($displayPath, 'File not found'); 2116 | } 2117 | 2118 | return $this->returnSingle($this->cachedPaths[$displayPath], $returnFirstItem); 2119 | } 2120 | 2121 | /** 2122 | * Convert virtual path to display path 2123 | * 2124 | * @param string $virtualPath 2125 | * @return string 2126 | * 2127 | * @throws UnableToReadFile 2128 | */ 2129 | protected function toDisplayPath($virtualPath) 2130 | { 2131 | if ($virtualPath === '' || $virtualPath === '/') { 2132 | return '/'; 2133 | } 2134 | 2135 | $tokens = explode('/', trim($virtualPath, '/')); 2136 | 2137 | /** @var DriveFile[] $objects */ 2138 | $objects = $this->getObjects($tokens); 2139 | $display = ''; 2140 | foreach ($tokens as $token) { 2141 | if (!isset($objects[$token])) { 2142 | throw UnableToReadFile::fromLocation($virtualPath, 'File not found'); 2143 | } 2144 | if (!empty($display) || $display === '0') { 2145 | $display .= '/'; 2146 | } 2147 | $display .= $this->sanitizeFilename($objects[$token]->getName()); 2148 | } 2149 | return $display; 2150 | } 2151 | 2152 | protected function toSingleVirtualPath($displayPath, $makeFullVirtualPath = true, $can_throw = true, $createDirsIfNeeded = false, $is_dir = false) 2153 | { 2154 | try { 2155 | $path = $this->toVirtualPath($displayPath, $makeFullVirtualPath, true); 2156 | } catch (UnableToReadFile $e) { 2157 | if (!$createDirsIfNeeded) { 2158 | if ($can_throw) { 2159 | throw $e; 2160 | } 2161 | return false; 2162 | } 2163 | 2164 | $subdir = $is_dir ? $displayPath : self::dirname($displayPath); 2165 | if ($subdir === '') { 2166 | if ($can_throw) { 2167 | throw $e; 2168 | } 2169 | return false; 2170 | } 2171 | 2172 | $this->createDirectory($subdir, new Config()); 2173 | if (!$this->hasDir($subdir)) { 2174 | if ($can_throw) { 2175 | throw $e; 2176 | } 2177 | return false; 2178 | } 2179 | 2180 | try { 2181 | $path = $this->toVirtualPath($displayPath, $makeFullVirtualPath, true); 2182 | } catch (UnableToReadFile $e) { 2183 | if ($can_throw) { 2184 | throw $e; 2185 | } 2186 | return false; 2187 | } 2188 | } 2189 | return $path; 2190 | } 2191 | 2192 | protected function canRequest($id, $is_full_req) 2193 | { 2194 | if (!isset($this->requestedIds[$id])) { 2195 | return true; 2196 | } 2197 | if ($is_full_req && $this->requestedIds[$id]['type'] === false) { 2198 | return true; 2199 | } // we're making a full dir request and previous request was dirs only...allow 2200 | if (time() - $this->requestedIds[$id]['time'] > self::FILE_OBJECT_MINIMUM_VALID_TIME) { 2201 | return true; 2202 | } 2203 | return false; // not yet 2204 | } 2205 | 2206 | protected function markRequest($id, $is_full_req) 2207 | { 2208 | $this->requestedIds[$id] = [ 2209 | 'type' => (bool)$is_full_req, 2210 | 'time' => time() 2211 | ]; 2212 | } 2213 | 2214 | /** 2215 | * @param string|string[] $id 2216 | * @param bool $reset_all 2217 | */ 2218 | protected function resetRequest($id, $reset_all = false) 2219 | { 2220 | if ($reset_all) { 2221 | $this->requestedIds = []; 2222 | } else { 2223 | if (is_array($id)) { 2224 | foreach ($id as $i) { 2225 | if ($i === $this->root) { 2226 | unset($this->requestedIds['']); 2227 | } 2228 | unset($this->requestedIds[$i]); 2229 | } 2230 | } else { 2231 | if ($id === $this->root) { 2232 | unset($this->requestedIds['']); 2233 | } 2234 | unset($this->requestedIds[$id]); 2235 | } 2236 | } 2237 | } 2238 | 2239 | protected function sanitizeFilename($filename) 2240 | { 2241 | if (!empty($this->options['sanitize_chars'])) { 2242 | $filename = str_replace( 2243 | $this->options['sanitize_chars'], 2244 | $this->options['sanitize_replacement_char'], 2245 | $filename 2246 | ); 2247 | } 2248 | 2249 | return $filename; 2250 | } 2251 | 2252 | public static function dirname($path) 2253 | { 2254 | // fix for Flysystem bug on Windows 2255 | $path = self::normalizeDirname(dirname($path)); 2256 | return str_replace('\\', '/', $path); 2257 | } 2258 | 2259 | protected function applyDefaultParams($params, $cmdName) 2260 | { 2261 | if (isset($this->optParams[$cmdName]) && is_array($this->optParams[$cmdName])) { 2262 | return array_replace($this->optParams[$cmdName], $params); 2263 | } else { 2264 | return $params; 2265 | } 2266 | } 2267 | 2268 | /** 2269 | * Enables empty google drive trash 2270 | * 2271 | * @return void 2272 | * 2273 | * @see https://developers.google.com/drive/v3/reference/files emptyTrash 2274 | * @see \Google_Service_Drive_Resource_Files 2275 | */ 2276 | public function emptyTrash(array $params = []) 2277 | { 2278 | $this->refreshToken(); 2279 | $this->service->files->emptyTrash($this->applyDefaultParams($params, 'files.emptyTrash')); 2280 | } 2281 | 2282 | /** 2283 | * Enables Team Drive support by changing default parameters 2284 | * 2285 | * @return void 2286 | * 2287 | * @see https://developers.google.com/drive/v3/reference/files 2288 | * @see \Google_Service_Drive_Resource_Files 2289 | */ 2290 | public function enableTeamDriveSupport() 2291 | { 2292 | $this->optParams = array_merge_recursive( 2293 | array_fill_keys([ 2294 | 'files.copy', 'files.create', 'files.delete', 2295 | 'files.trash', 'files.get', 'files.list', 'files.update', 2296 | 'files.watch', 'permissions.list' 2297 | ], ['supportsAllDrives' => true]), 2298 | $this->optParams 2299 | ); 2300 | } 2301 | 2302 | /** 2303 | * Selects Team Drive to operate by changing default parameters 2304 | * 2305 | * @param string $teamDriveId Team Drive id 2306 | * @param string $corpora Corpora value for files.list 2307 | * @return void 2308 | * 2309 | * @see https://developers.google.com/drive/v3/reference/files 2310 | * @see https://developers.google.com/drive/v3/reference/files/list 2311 | * @see \Google_Service_Drive_Resource_Files 2312 | */ 2313 | public function setTeamDriveId($teamDriveId, $corpora = 'drive') 2314 | { 2315 | $this->enableTeamDriveSupport(); 2316 | $this->optParams = array_merge_recursive($this->optParams, [ 2317 | 'files.list' => [ 2318 | 'corpora' => $corpora, 2319 | 'includeItemsFromAllDrives' => true, 2320 | 'driveId' => $teamDriveId 2321 | ] 2322 | ]); 2323 | 2324 | if ($this->root === 'root' || $this->root === null) { 2325 | $this->setPathPrefix(''); 2326 | $this->root = $teamDriveId; 2327 | } 2328 | } 2329 | 2330 | /** 2331 | * Guess MIME Type based on the path of the file and it's content. 2332 | * 2333 | * @param string $path 2334 | * @param string|resource $content 2335 | * @return string|null MIME Type or NULL if no extension detected 2336 | */ 2337 | public static function guessMimeType($path, $content) 2338 | { 2339 | $detector = new FinfoMimeTypeDetector(); 2340 | if (is_string($content)) { 2341 | $mimeType = $detector->detectMimeTypeFromBuffer($content); 2342 | } 2343 | if (!(empty($mimeType) || in_array($mimeType, ['application/x-empty', 'text/plain', 'text/x-asm']))) { 2344 | return $mimeType; 2345 | } 2346 | return $detector->detectMimeTypeFromPath($path) ?: 'text/plain'; 2347 | } 2348 | 2349 | /** 2350 | * Normalize a dirname return value. 2351 | * 2352 | * @param string $dirname 2353 | * @return string normalized dirname 2354 | */ 2355 | public static function normalizeDirname($dirname) 2356 | { 2357 | return $dirname === '.' ? '' : $dirname; 2358 | } 2359 | } 2360 | --------------------------------------------------------------------------------