{"id":20349405,"url":"https://github.com/rootinc/laravel-s3-file-model","last_synced_at":"2025-10-15T17:55:29.166Z","repository":{"id":53803918,"uuid":"274500826","full_name":"rootinc/laravel-s3-file-model","owner":"rootinc","description":null,"archived":false,"fork":false,"pushed_at":"2023-03-03T20:18:05.000Z","size":38,"stargazers_count":0,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-10-06T00:42:48.531Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rootinc.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-06-23T20:19:23.000Z","updated_at":"2023-02-23T17:13:46.000Z","dependencies_parsed_at":"2024-11-15T01:01:32.290Z","dependency_job_id":null,"html_url":"https://github.com/rootinc/laravel-s3-file-model","commit_stats":{"total_commits":33,"total_committers":4,"mean_commits":8.25,"dds":0.2727272727272727,"last_synced_commit":"d5e3d0db0ad44f5d5d577940990f6bd197e4f443"},"previous_names":[],"tags_count":12,"template":false,"template_full_name":null,"purl":"pkg:github/rootinc/laravel-s3-file-model","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rootinc%2Flaravel-s3-file-model","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rootinc%2Flaravel-s3-file-model/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rootinc%2Flaravel-s3-file-model/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rootinc%2Flaravel-s3-file-model/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rootinc","download_url":"https://codeload.github.com/rootinc/laravel-s3-file-model/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rootinc%2Flaravel-s3-file-model/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279098016,"owners_count":26102937,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-15T02:00:07.814Z","response_time":56,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-14T22:25:47.399Z","updated_at":"2025-10-15T17:55:29.144Z","avatar_url":"https://github.com/rootinc.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Laravel S3 File Model\n\nProvides a File Model that supports direct uploads / downloads from S3 for a Laravel App.\n\n## Installation\n\n1. `composer require rootinc/laravel-s3-file-model`\n2. Run `php artisan vendor:publish --provider=\"RootInc\\LaravelS3FileModel\\FileModelServiceProvider\"` to create `File` model in `app`, `FileTest` in `tests\\Unit`, `FileFactory` in `database\\factories`, `2020_03_12_152841_create_files_table` in `database\\migrations`, and `FileController` in `app\\Http\\Controllers`\n3. Run `php artisan vendor:publish  --provider=\"Aws\\Laravel\\AwsServiceProvider` which adds `aws.php` in the `config` folder\n4. In the `aws.php` file, change `'region' =\u003e env('AWS_REGION', 'us-east-1'),` to use `AWS_DEFAULT_REGION`\n5. In `config\\filesystems.php`, add key `'directory' =\u003e '', // root dir` to `public` and add key `'directory' =\u003e env('AWS_UPLOAD_FOLDER'),` to `s3`\n6. In `tests\\TestCase`, add this function:\n```\nprotected function get1x1RedPixelImage()\n{\n    return \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==\";\n}\n```\n7. Update routing.  We can use this as an example: `Route::apiResource('files', 'FileController')-\u003eonly(['index', 'store', 'update', 'destroy']);`\n8. :tada:\n\n## Update from 0.1.* to 0.2.*\n\nIf we are using API versioning paradigm, 0.2.* lets us abstract the file model so that in a child class, we can import a different version of the File.\n\nFor example, our `FileController` would look something like this:\n\n```\n\u003c?php\n\nnamespace App\\Http\\Controllers\\Api\\v3;\n\nuse RootInc\\LaravelS3FileModel\\FileBaseController;\n\nuse ReflectionClass;\n\nuse App\\Models\\v3\\File;\n\nclass FileController extends FileBaseController\n{\n    protected static function getFileModel()\n    {\n        $rc = new ReflectionClass(File::class);\n        return $rc-\u003enewInstance();\n    }\n...\n```\n\nNow when running the parent's functionality, it will make use of the right model.\n\nNote -- the FileController's `update` and `delete` methods have been switched from `public function method(Request $request, File $file)` to `public function method(Request $request, $file_id)`\n\nThe same has been updated for tests.  That way, if we want to make use of Laravel 8's updated factory classes, we can do so like below:\n\n```\n\u003c?php\n\nnamespace Tests\\Unit\\v2;\n\nuse RootInc\\LaravelS3FileModel\\FileModelTest;\n\nuse Tests\\DatabaseMigrationsUpTo;\n\nuse ReflectionClass;\n\nuse App\\Models\\v2\\File;\n\nclass FileTest extends FileModelTest\n{\n    use DatabaseMigrationsUpTo;\n\n    protected static function getFileModel()\n    {\n        $rc = new ReflectionClass(File::class);\n        return $rc-\u003enewInstance();\n    }\n\n    protected function getFileFactory($count=1, $create=true, $properties=[])\n    {\n        $files;\n\n        $factory = File::factory()-\u003ecount($count);\n        if ($create)\n        {\n            $files = $factory-\u003ecreate($properties);\n        }\n        else\n        {\n            $files = $factory-\u003emake($properties);\n        }\n\n        $len = count($files);\n        if ($len === 1)\n        {\n            return $files[0];\n        }\n        else if ($len === 0)\n        {\n            return null;\n        }\n        else\n        {\n            return $files;\n        }\n    }\n...\n```\n\n## Example React FileUploader\n\n```\nimport React, { useState, useEffect, useRef } from 'react';\nimport ReactDOM from 'react-dom';\nimport PropTypes from 'prop-types';\nimport api from '../../helpers/api';\n\nconst propTypes = {\n  afterSuccess: PropTypes.func,\n  file: PropTypes.object,\n  cloudUpload: PropTypes.bool,\n  style: PropTypes.object,\n  public: PropTypes.bool,\n};\n\nconst defaultProps = {\n  afterSuccess: () =\u003e {},\n  cloudUpload: false,\n  style: {},\n  public: false,\n};\n\nfunction FileUploader(props){\n  const elInput = useRef(null);\n\n  const [file, setFile] = useState(null);\n  const [draggingState, setDraggingState] = useState(false);\n  const [percentCompleted, setPercentCompleted] = useState(null);\n\n  // Use dependency on props.file for when we load an existing file\n  useEffect(() =\u003e {\n    setFile(props.file)\n  }, [props.file]);\n\n  const dragOver = () =\u003e {\n    if (percentCompleted === null)\n    {\n      setDraggingState(true)\n    }\n  };\n\n  const dragEnd = () =\u003e {\n    setDraggingState(false)\n  };\n\n  const nullImportValue = () =\u003e {\n    ReactDOM.findDOMNode(elInput.current).value = null;\n  };\n\n  const handleChange = (blob) =\u003e {\n    const reader = new FileReader();\n\n    reader.addEventListener(\"load\", () =\u003e {\n      if (props.cloudUpload)\n      {\n        pingUpload({\n          file_name: blob.name,\n          file_type: blob.type,\n          public: props.public,\n        }, blob); //XMLHttpRequest can take a raw file blob, which works better for streaming the file\n      }\n      else\n      {\n        upload({\n          file_name: blob.name,\n          file_type: blob.type,\n          file_data: reader.result,\n          public: props.public,\n        });\n      }\n    }, false);\n\n    reader.readAsDataURL(blob);\n  };\n\n  const pingUpload = async (data, blob) =\u003e {\n    const response = file\n      ? await api.putFile(file.id, data)\n      : await api.postFile(data)\n\n    response.ok\n      ? cloudUpload(response, blob)\n      : error(response)\n  }\n\n  const cloudUpload = async (response, blob) =\u003e {\n    const putCloudObject = () =\u003e {\n      return new Promise((resolve, reject) =\u003e {\n        const xhr = new XMLHttpRequest();\n        xhr.open(\"PUT\", response.data.payload.upload_url);\n        xhr.setRequestHeader(\"Content-Type\", response.data.payload.file.file_type);\n        xhr.setRequestHeader(\"Cache-Control\", `max-age=${60*60*7}`); //cache for a week (in case a developer uploads with disable cache checked)\n\n        xhr.onload = () =\u003e {\n          resolve(xhr);\n        };\n\n        xhr.onerror = () =\u003e {\n          reject(new Error(xhr.statusText));\n        };\n\n        xhr.upload.onprogress = (e) =\u003e {\n          const percentCompleted = Math.round( (e.loaded / e.total) * 100 );\n          setPercentCompleted(percentCompleted);\n        };\n\n        //thankfully blobs can be sent up, and this works better https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/send\n        xhr.send(blob);\n      });\n    }\n\n    const cloudResponse = await putCloudObject();\n\n    cloudResponse.status === 200\n      ? success(response)\n      : error(cloudResponse.response)\n  }\n\n  const upload = async (data) =\u003e {\n    const config = {\n      onUploadProgress: (e) =\u003e {\n        const percentCompleted = Math.round( (e.loaded / e.total) * 100 );\n        setPercentCompleted(percentCompleted);\n      }\n    };\n\n    const response = file\n      ? await api.putFile(file.id, data, config)\n      : await api.postFile(data, config)\n\n    response.ok\n      ? success(response)\n      : error(response)\n  };\n\n  const success = (response) =\u003e {\n    nullImportValue();\n\n    if (response.data.status === \"success\")\n    {\n      setFile(response.data.payload.file)\n    }\n    else\n    {\n      alert(response.data.payload.errors[0]);\n    }\n    setPercentCompleted(null)\n    props.afterSuccess(response.data.payload.file)\n  };\n\n  const error = (error) =\u003e {\n    nullImportValue();\n    setPercentCompleted(null)\n\n    alert(window._genericErrorMessage);\n  };\n\n  const renderInstructions = () =\u003e {\n    if (percentCompleted === null)\n    {\n      return (\n        \u003cp\n          style={{\n            cursor: \"pointer\"\n          }}\n          onClick={() =\u003e {\n            ReactDOM.findDOMNode(elInput.current).click();\n          }}\n        \u003e\n          \u003cstrong\u003e{file ? \"Replace\" : \"Choose\"} File\u003c/strong\u003e or drag it here.\n        \u003c/p\u003e\n      );\n    }\n    else if (percentCompleted \u003c 100)\n    {\n      return (\n        \u003cprogress\n          value={percentCompleted}\n          max=\"100\"\n        \u003e\n          {percentCompleted}%\n        \u003c/progress\u003e\n      );\n    }\n    else\n    {\n      return \u003ci className=\"fa fa-cog fa-spin fa-3x fa-fw\" aria-hidden=\"true\" /\u003e;\n    }\n  };\n\n  const renderFileInfo = () =\u003e {\n    if (file)\n    {\n      return (\n        \u003cdiv\u003e\n          \u003cp\n            style={{\n              marginBottom: 0\n            }}\n          \u003e\n            Current File:\u0026nbsp;\n            \u003ca\n              href={file.fullUrl}\n              target=\"_blank\"\n              style={{\n                wordBreak: \"break-all\"\n              }}\n            \u003e\n              {file.title}\n            \u003c/a\u003e\n            \u003cbutton\n              style={{\n                marginLeft: \"10px\",\n                backgroundColor: \"gray\",\n                padding: \".45rem .5rem .3rem .5rem\"\n              }}\n              onClick={async () =\u003e {\n                const result = prompt(\"New Title?\", file.title);\n                if (result)\n                {\n                  const response = await api.putFile(file.id, {title: result});\n\n                  response.ok\n                    ? success(response)\n                    : error(response)\n                }\n              }}\n            \u003e\n              Rename\n            \u003c/button\u003e\n          \u003c/p\u003e\n          \u003cp\n            style={{\n              marginTop: 0,\n              wordBreak: \"break-all\"\n            }}\n          \u003e\n            Original Name: {file.file_name}\n          \u003c/p\u003e\n        \u003c/div\u003e\n      );\n    }\n    else\n    {\n      return null;\n    }\n  }\n\n  const style = Object.assign({\n    border: \"2px dashed black\",\n    borderRadius: \"10px\",\n    backgroundColor: draggingState ? \"white\" : \"lightgray\",\n    height: \"250px\",\n    display: \"flex\",\n    flexDirection: \"column\",\n    justifyContent: \"center\",\n    alignItems: \"center\",\n  }, props.style);\n\n  return (\n    \u003cdiv\n      style={style}\n      onClick={(e) =\u003e {e.stopPropagation();}}\n      onDrag={(e) =\u003e {e.preventDefault();}}\n      onDragStart={(e) =\u003e {e.preventDefault();}}\n      onDragEnd={(e) =\u003e {e.preventDefault(); dragEnd();}}\n      onDragOver={(e) =\u003e {e.preventDefault(); dragOver();}}\n      onDragEnter={(e) =\u003e {e.preventDefault(); dragOver();}}\n      onDragLeave={(e) =\u003e {e.preventDefault(); dragEnd();}}\n      onDrop={(e) =\u003e {\n        e.preventDefault();\n        dragEnd();\n\n        if (percentCompleted === null)\n        {\n          const droppedFiles = e.dataTransfer.files;\n          handleChange(droppedFiles[0]);\n        }\n      }}\n    \u003e\n      \u003ci className=\"fa fa-upload\" aria-hidden=\"true\" /\u003e\n      {\n        renderInstructions()\n      }\n      {\n        renderFileInfo()\n      }\n      \u003cinput\n        ref={elInput}\n        className=\"file-uploader\"\n        type=\"file\"\n        style={{\n          position: \"fixed\",\n          top: \"-100em\"\n        }}\n        onChange={(e) =\u003e {\n          handleChange(e.target.files[0]);\n        }}\n      /\u003e\n    \u003c/div\u003e\n  );\n}\n\nFileUploader.propTypes = propTypes;\nFileUploader.defaultProps = defaultProps;\n\nexport default FileUploader;\n```\n\n## Contributing\n\nThank you for considering contributing to the Laravel S3 File Model! To encourage active collaboration, we encourage pull requests, not just issues.\n\nIf you file an issue, the issue should contain a title and a clear description of the issue. You should also include as much relevant information as possible and a code sample that demonstrates the issue. The goal of a issue is to make it easy for yourself - and others - to replicate the bug and develop a fix.\n\n## License\n\nThe Laravel S3 File Model is open-sourced software licensed under the [MIT license](http://opensource.org/licenses/MIT).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frootinc%2Flaravel-s3-file-model","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frootinc%2Flaravel-s3-file-model","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frootinc%2Flaravel-s3-file-model/lists"}