rootinc / laravel-s3-file-model
S3 文件模型
1.1.1
2023-03-03 20:17 UTC
Requires
- php: >=8.0.2
- aws/aws-sdk-php-laravel: ^3
- laravel/framework: >=9
- league/flysystem-aws-s3-v3: ^3
- maltyxx/images-generator: ^1
README
为 Laravel 应用提供支持直接从 S3 上传/下载文件的文件模型。
安装
composer require rootinc/laravel-s3-file-model
- 运行
php artisan vendor:publish --provider="RootInc\LaravelS3FileModel\FileModelServiceProvider"
以在app
中创建File
模型,在tests\Unit
中创建FileTest
,在database\factories
中创建FileFactory
,在database\migrations
中创建2020_03_12_152841_create_files_table
,以及在app\Http\Controllers
中创建FileController
- 运行
php artisan vendor:publish --provider="Aws\Laravel\AwsServiceProvider"
以在config
文件夹中添加aws.php
- 在
aws.php
文件中,将'region' => env('AWS_REGION', 'us-east-1'),
修改为使用AWS_DEFAULT_REGION
- 在
config\filesystems.php
中,为public
添加键'directory' => '', // 根目录
,并为s3
添加键'directory' => env('AWS_UPLOAD_FOLDER'),
- 在
tests\TestCase
中添加此函数
protected function get1x1RedPixelImage()
{
return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==";
}
- 更新路由。以下是一个示例:
Route::apiResource('files', 'FileController')->only(['index', 'store', 'update', 'destroy']);
- 🎉
从 0.1.* 更新到 0.2.*
如果我们使用 API 版本化范式,0.2.* 允许我们抽象文件模型,以便在子类中导入不同的文件版本。
例如,我们的 FileController
可能看起来像这样
<?php
namespace App\Http\Controllers\Api\v3;
use RootInc\LaravelS3FileModel\FileBaseController;
use ReflectionClass;
use App\Models\v3\File;
class FileController extends FileBaseController
{
protected static function getFileModel()
{
$rc = new ReflectionClass(File::class);
return $rc->newInstance();
}
...
现在当运行父类功能时,它将使用正确的模型。
注意 -- FileController 的 update
和 delete
方法已从 public function method(Request $request, File $file)
更改为 public function method(Request $request, $file_id)
测试也已进行了相同的更新。这样,如果我们想使用 Laravel 8 的更新工厂类,可以像以下这样做
<?php
namespace Tests\Unit\v2;
use RootInc\LaravelS3FileModel\FileModelTest;
use Tests\DatabaseMigrationsUpTo;
use ReflectionClass;
use App\Models\v2\File;
class FileTest extends FileModelTest
{
use DatabaseMigrationsUpTo;
protected static function getFileModel()
{
$rc = new ReflectionClass(File::class);
return $rc->newInstance();
}
protected function getFileFactory($count=1, $create=true, $properties=[])
{
$files;
$factory = File::factory()->count($count);
if ($create)
{
$files = $factory->create($properties);
}
else
{
$files = $factory->make($properties);
}
$len = count($files);
if ($len === 1)
{
return $files[0];
}
else if ($len === 0)
{
return null;
}
else
{
return $files;
}
}
...
React 文件上传器示例
import React, { useState, useEffect, useRef } from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import api from '../../helpers/api';
const propTypes = {
afterSuccess: PropTypes.func,
file: PropTypes.object,
cloudUpload: PropTypes.bool,
style: PropTypes.object,
public: PropTypes.bool,
};
const defaultProps = {
afterSuccess: () => {},
cloudUpload: false,
style: {},
public: false,
};
function FileUploader(props){
const elInput = useRef(null);
const [file, setFile] = useState(null);
const [draggingState, setDraggingState] = useState(false);
const [percentCompleted, setPercentCompleted] = useState(null);
// Use dependency on props.file for when we load an existing file
useEffect(() => {
setFile(props.file)
}, [props.file]);
const dragOver = () => {
if (percentCompleted === null)
{
setDraggingState(true)
}
};
const dragEnd = () => {
setDraggingState(false)
};
const nullImportValue = () => {
ReactDOM.findDOMNode(elInput.current).value = null;
};
const handleChange = (blob) => {
const reader = new FileReader();
reader.addEventListener("load", () => {
if (props.cloudUpload)
{
pingUpload({
file_name: blob.name,
file_type: blob.type,
public: props.public,
}, blob); //XMLHttpRequest can take a raw file blob, which works better for streaming the file
}
else
{
upload({
file_name: blob.name,
file_type: blob.type,
file_data: reader.result,
public: props.public,
});
}
}, false);
reader.readAsDataURL(blob);
};
const pingUpload = async (data, blob) => {
const response = file
? await api.putFile(file.id, data)
: await api.postFile(data)
response.ok
? cloudUpload(response, blob)
: error(response)
}
const cloudUpload = async (response, blob) => {
const putCloudObject = () => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("PUT", response.data.payload.upload_url);
xhr.setRequestHeader("Content-Type", response.data.payload.file.file_type);
xhr.setRequestHeader("Cache-Control", `max-age=${60*60*7}`); //cache for a week (in case a developer uploads with disable cache checked)
xhr.onload = () => {
resolve(xhr);
};
xhr.onerror = () => {
reject(new Error(xhr.statusText));
};
xhr.upload.onprogress = (e) => {
const percentCompleted = Math.round( (e.loaded / e.total) * 100 );
setPercentCompleted(percentCompleted);
};
//thankfully blobs can be sent up, and this works better https://mdn.org.cn/en-US/docs/Web/API/XMLHttpRequest/send
xhr.send(blob);
});
}
const cloudResponse = await putCloudObject();
cloudResponse.status === 200
? success(response)
: error(cloudResponse.response)
}
const upload = async (data) => {
const config = {
onUploadProgress: (e) => {
const percentCompleted = Math.round( (e.loaded / e.total) * 100 );
setPercentCompleted(percentCompleted);
}
};
const response = file
? await api.putFile(file.id, data, config)
: await api.postFile(data, config)
response.ok
? success(response)
: error(response)
};
const success = (response) => {
nullImportValue();
if (response.data.status === "success")
{
setFile(response.data.payload.file)
}
else
{
alert(response.data.payload.errors[0]);
}
setPercentCompleted(null)
props.afterSuccess(response.data.payload.file)
};
const error = (error) => {
nullImportValue();
setPercentCompleted(null)
alert(window._genericErrorMessage);
};
const renderInstructions = () => {
if (percentCompleted === null)
{
return (
<p
style={{
cursor: "pointer"
}}
onClick={() => {
ReactDOM.findDOMNode(elInput.current).click();
}}
>
<strong>{file ? "Replace" : "Choose"} File</strong> or drag it here.
</p>
);
}
else if (percentCompleted < 100)
{
return (
<progress
value={percentCompleted}
max="100"
>
{percentCompleted}%
</progress>
);
}
else
{
return <i className="fa fa-cog fa-spin fa-3x fa-fw" aria-hidden="true" />;
}
};
const renderFileInfo = () => {
if (file)
{
return (
<div>
<p
style={{
marginBottom: 0
}}
>
Current File:
<a
href={file.fullUrl}
target="_blank"
style={{
wordBreak: "break-all"
}}
>
{file.title}
</a>
<button
style={{
marginLeft: "10px",
backgroundColor: "gray",
padding: ".45rem .5rem .3rem .5rem"
}}
onClick={async () => {
const result = prompt("New Title?", file.title);
if (result)
{
const response = await api.putFile(file.id, {title: result});
response.ok
? success(response)
: error(response)
}
}}
>
Rename
</button>
</p>
<p
style={{
marginTop: 0,
wordBreak: "break-all"
}}
>
Original Name: {file.file_name}
</p>
</div>
);
}
else
{
return null;
}
}
const style = Object.assign({
border: "2px dashed black",
borderRadius: "10px",
backgroundColor: draggingState ? "white" : "lightgray",
height: "250px",
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
}, props.style);
return (
<div
style={style}
onClick={(e) => {e.stopPropagation();}}
onDrag={(e) => {e.preventDefault();}}
onDragStart={(e) => {e.preventDefault();}}
onDragEnd={(e) => {e.preventDefault(); dragEnd();}}
onDragOver={(e) => {e.preventDefault(); dragOver();}}
onDragEnter={(e) => {e.preventDefault(); dragOver();}}
onDragLeave={(e) => {e.preventDefault(); dragEnd();}}
onDrop={(e) => {
e.preventDefault();
dragEnd();
if (percentCompleted === null)
{
const droppedFiles = e.dataTransfer.files;
handleChange(droppedFiles[0]);
}
}}
>
<i className="fa fa-upload" aria-hidden="true" />
{
renderInstructions()
}
{
renderFileInfo()
}
<input
ref={elInput}
className="file-uploader"
type="file"
style={{
position: "fixed",
top: "-100em"
}}
onChange={(e) => {
handleChange(e.target.files[0]);
}}
/>
</div>
);
}
FileUploader.propTypes = propTypes;
FileUploader.defaultProps = defaultProps;
export default FileUploader;
贡献
感谢您考虑为 Laravel S3 文件模型做出贡献!为了鼓励积极的协作,我们鼓励提交拉取请求,而不仅仅是问题。
如果您提交问题,问题应包含标题和问题的清晰描述。您还应包括尽可能多的相关信息和一个演示问题的代码示例。问题的目标是让您和他人能够轻松地复制错误并开发修复方案。
许可
Laravel S3 文件模型是开源软件,许可协议为 MIT 许可证。