rootinc/laravel-s3-file-model

S3 文件模型

1.1.1 2023-03-03 20:17 UTC

This package is auto-updated.

Last update: 2024-08-30 01:11:54 UTC


README

为 Laravel 应用提供支持直接从 S3 上传/下载文件的文件模型。

安装

  1. composer require rootinc/laravel-s3-file-model
  2. 运行 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
  3. 运行 php artisan vendor:publish --provider="Aws\Laravel\AwsServiceProvider" 以在 config 文件夹中添加 aws.php
  4. aws.php 文件中,将 'region' => env('AWS_REGION', 'us-east-1'), 修改为使用 AWS_DEFAULT_REGION
  5. config\filesystems.php 中,为 public 添加键 'directory' => '', // 根目录,并为 s3 添加键 'directory' => env('AWS_UPLOAD_FOLDER'),
  6. tests\TestCase 中添加此函数
protected function get1x1RedPixelImage()
{
    return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==";
}
  1. 更新路由。以下是一个示例:Route::apiResource('files', 'FileController')->only(['index', 'store', 'update', 'destroy']);
  2. 🎉

从 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 的 updatedelete 方法已从 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:&nbsp;
            <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 许可证