go upload

main
鹿和sa0ChunLuyu 3 days ago
parent 184f05a7f7
commit 306a1bb179

@ -22,6 +22,9 @@ QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
# Go 文件上传服务地址
GO_UPLOAD_SERVICE=http://127.0.0.1:8852
MEMCACHED_HOST=127.0.0.1
REDIS_HOST=127.0.0.1

@ -0,0 +1,388 @@
<?php
namespace App\Filesystem;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use League\Flysystem\Adapter\AbstractAdapter;
use League\Flysystem\Config;
class HttpGoAdapter extends AbstractAdapter
{
protected Client $client;
protected string $baseUri;
public function __construct(string $baseUri)
{
$this->baseUri = rtrim($baseUri, '/');
$this->client = new Client([
'base_uri' => $this->baseUri . '/',
'timeout' => 30,
'http_errors' => false,
]);
}
/**
* 写入文件base64 方式)
*/
public function write($path, $contents, Config $config): array|false
{
$fullPath = $this->applyPathPrefix($path);
$dir = dirname($fullPath);
$filename = basename($fullPath);
try {
$response = $this->client->post('api/upload/base64', [
'json' => [
'file' => base64_encode($contents),
'path' => $dir,
'filename' => $filename,
],
]);
if ($response->getStatusCode() !== 200) {
return false;
}
$body = json_decode($response->getBody(), true);
if (($body['code'] ?? 0) !== 200) {
return false;
}
return $this->normalizeMetadata($fullPath, $config);
} catch (GuzzleException $e) {
return false;
}
}
/**
* 写入文件流multipart 方式)
*/
public function writeStream($path, $resource, Config $config): array|false
{
$fullPath = $this->applyPathPrefix($path);
$dir = dirname($fullPath);
$filename = basename($fullPath);
// 将流转换为临时文件
$tmpFile = tmpfile();
stream_copy_to_stream($resource, $tmpFile);
$meta = stream_get_meta_data($tmpFile);
try {
$response = $this->client->post('api/upload/file', [
'multipart' => [
[
'name' => 'file',
'contents' => fopen($meta['uri'], 'r'),
'filename' => $filename,
],
[
'name' => 'path',
'contents' => $dir,
],
],
]);
fclose($tmpFile);
if ($response->getStatusCode() !== 200) {
return false;
}
$body = json_decode($response->getBody(), true);
if (($body['code'] ?? 0) !== 200) {
return false;
}
return $this->normalizeMetadata($fullPath, $config);
} catch (GuzzleException $e) {
fclose($tmpFile);
return false;
}
}
/**
* 更新文件(委托给 write
*/
public function update($path, $contents, Config $config): array|false
{
return $this->write($path, $contents, $config);
}
/**
* 更新文件流(委托给 writeStream
*/
public function updateStream($path, $resource, Config $config): array|false
{
return $this->writeStream($path, $resource, $config);
}
/**
* 读取文件内容
*/
public function read($path): array|false
{
$fullPath = $this->applyPathPrefix($path);
try {
$response = $this->client->get('files/' . ltrim($fullPath, '/'));
if ($response->getStatusCode() !== 200) {
return false;
}
return ['contents' => $response->getBody()->getContents()];
} catch (GuzzleException $e) {
return false;
}
}
/**
* 读取文件流
*/
public function readStream($path): array|false
{
$fullPath = $this->applyPathPrefix($path);
try {
$response = $this->client->get('files/' . ltrim($fullPath, '/'), [
'stream' => true,
]);
if ($response->getStatusCode() !== 200) {
return false;
}
// 返回一个临时流
$tmpFile = tmpfile();
$stream = $response->getBody()->detach();
if ($stream === null) {
fclose($tmpFile);
return false;
}
stream_copy_to_stream($stream, $tmpFile);
fclose($stream);
rewind($tmpFile);
return ['stream' => $tmpFile];
} catch (GuzzleException $e) {
return false;
}
}
/**
* 检查文件是否存在
*/
public function has($path): bool
{
$fullPath = $this->applyPathPrefix($path);
try {
$response = $this->client->head('files/' . ltrim($fullPath, '/'));
return $response->getStatusCode() === 200;
} catch (GuzzleException $e) {
return false;
}
}
/**
* 删除文件
*/
public function delete($path): bool
{
$fullPath = $this->applyPathPrefix($path);
try {
$response = $this->client->delete('files/' . ltrim($fullPath, '/'));
return $response->getStatusCode() === 200;
} catch (GuzzleException $e) {
return false;
}
}
/**
* 删除目录
*/
public function deleteDir($dirname): bool
{
$fullPath = $this->applyPathPrefix($dirname);
try {
$response = $this->client->delete('files/' . ltrim($fullPath, '/'));
return $response->getStatusCode() === 200;
} catch (GuzzleException $e) {
return false;
}
}
/**
* 创建目录Go 服务隐式创建,直接返回成功)
*/
public function createDir($dirname, Config $config): array|false
{
return ['path' => $dirname, 'type' => 'dir'];
}
/**
* 设置可见性(始终 public
*/
public function setVisibility($path, $visibility): array|false
{
return ['visibility' => $visibility, 'path' => $path];
}
/**
* 获取可见性
*/
public function getVisibility($path): array|false
{
return ['visibility' => 'public', 'path' => $path];
}
/**
* 获取 MIME 类型
*/
public function getMimetype($path): array|false
{
$fullPath = $this->applyPathPrefix($path);
try {
$response = $this->client->head('files/' . ltrim($fullPath, '/'));
if ($response->getStatusCode() !== 200) {
return false;
}
$contentType = $response->getHeader('Content-Type')[0] ?? 'application/octet-stream';
return ['mimetype' => $contentType];
} catch (GuzzleException $e) {
return false;
}
}
/**
* 获取文件大小
*/
public function getSize($path): array|false
{
$fullPath = $this->applyPathPrefix($path);
try {
$response = $this->client->head('files/' . ltrim($fullPath, '/'));
if ($response->getStatusCode() !== 200) {
return false;
}
$size = (int)($response->getHeader('Content-Length')[0] ?? 0);
return ['size' => $size];
} catch (GuzzleException $e) {
return false;
}
}
/**
* 获取文件修改时间
*/
public function getTimestamp($path): array|false
{
$fullPath = $this->applyPathPrefix($path);
try {
$response = $this->client->head('files/' . ltrim($fullPath, '/'));
if ($response->getStatusCode() !== 200) {
return false;
}
$lastModified = $response->getHeader('Last-Modified')[0] ?? '';
$timestamp = $lastModified ? strtotime($lastModified) : time();
return ['timestamp' => $timestamp];
} catch (GuzzleException $e) {
return false;
}
}
/**
* 获取文件元信息
*/
public function getMetadata($path): array|false
{
$fullPath = $this->applyPathPrefix($path);
try {
$response = $this->client->head('files/' . ltrim($fullPath, '/'));
if ($response->getStatusCode() !== 200) {
return false;
}
$contentType = $response->getHeader('Content-Type')[0] ?? 'application/octet-stream';
$size = (int)($response->getHeader('Content-Length')[0] ?? 0);
$lastModified = $response->getHeader('Last-Modified')[0] ?? '';
$timestamp = $lastModified ? strtotime($lastModified) : time();
return [
'type' => 'file',
'path' => $fullPath,
'timestamp' => $timestamp,
'size' => $size,
'mimetype' => $contentType,
];
} catch (GuzzleException $e) {
return false;
}
}
/**
* 列出目录内容(简化实现,返回空数组)
*/
public function listContents($directory = '', $recursive = false): array
{
return [];
}
/**
* 重命名文件
*/
public function rename($path, $newpath): bool
{
$contents = $this->read($path);
if ($contents === false) {
return false;
}
if ($this->write($newpath, $contents['contents'], new Config()) === false) {
return false;
}
return $this->delete($path);
}
/**
* 复制文件
*/
public function copy($path, $newpath): bool
{
$contents = $this->read($path);
if ($contents === false) {
return false;
}
return $this->write($newpath, $contents['contents'], new Config()) !== false;
}
/**
* 标准化元数据
*/
protected function normalizeMetadata(string $path, Config $config): array
{
return [
'type' => 'file',
'path' => $path,
'timestamp' => time(),
'size' => 0,
];
}
}

@ -38,4 +38,43 @@ public function UpFileBase64()
return \Yz::Return(true,'上传成功',['fileurl' =>'/storage/H5Upload/'.$date.'/'.$filename]);
}
/**
* 测试 network_drive 上传链路
* 上传文件到 Go 服务,返回可访问的完整 URL
* 入参: file (必填), disk_name (可选, 默认 network_drive_test)
*/
public function testUploadNetworkDrive()
{
$file = request('file');
$diskName = request('disk_name', 'network_drive_test');
if (!$file || !$file->isValid()) {
return \Yz::echoError1('获取文件失败');
}
// 验证文件类型
$mime = $file->getMimeType();
$allowed = ['application/pdf', 'image/png', 'image/jpeg', 'image/jpg'];
if (!in_array($mime, $allowed)) {
return \Yz::echoError1('不支持的文件格式,仅支持 PDF/PNG/JPG');
}
// 生成存储路径
$date = date('Ymd');
$filename = uniqid() . '_' . mt_rand(0, 999999) . '.' . $file->extension();
$filePath = '/phpUpload/test/' . $date . '/' . $filename;
// 根据传入的磁盘名称选择对应磁盘
$disk = Storage::disk($diskName);
$contents = file_get_contents($file->getRealPath());
$savePath = $disk->put($filePath, $contents);
$url = $disk->url($filePath);
return \Yz::Return(true, '上传成功', [
'disk' => $diskName,
'path' => $savePath,
'url' => $url,
]);
}
}

@ -2,7 +2,10 @@
namespace App\Providers;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\ServiceProvider;
use League\Flysystem\Filesystem;
use App\Filesystem\HttpGoAdapter;
class AppServiceProvider extends ServiceProvider
{
@ -23,6 +26,11 @@ public function register()
*/
public function boot()
{
//
Storage::extend('http_go', function($app, $config) {
$adapter = new HttpGoAdapter($config['base_uri']);
$filesystem = new Filesystem($adapter);
// 直接返回 FilesystemAdapter 并传入 config确保 url() 方法能读取配置
return new \Illuminate\Filesystem\FilesystemAdapter($filesystem, $config);
});
}
}

@ -53,8 +53,14 @@
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
],
'network_drive' => [
'driver' => 'local',
'root' => 'D:/aa', // Windows 映射的盘符路径
'driver' => 'http_go',
'base_uri' => env('GO_UPLOAD_SERVICE', 'http://127.0.0.1:8852'),
'url' => env('APP_URL').'/phpstorage',
'visibility' => 'public',
],
'network_drive_test' => [
'driver' => 'http_go',
'base_uri' => env('GO_UPLOAD_SERVICE', 'http://127.0.0.1:8852'),
'url' => env('APP_URL').'/phpstorage',
'visibility' => 'public',
],

@ -128,6 +128,9 @@
Route::post('healthCard/createInfo','App\Http\Controllers\API\Internal\HealthCertificateController@CreatInfo')->middleware('log');
Route::post('healthCard/uploadPdf','App\Http\Controllers\API\Internal\HealthCertificateController@uploadPdf')->middleware('log');
// 测试接口:验证 network_drive 上传链路
Route::post('testUploadNetworkDrive','App\Http\Controllers\API\UpLoadController@testUploadNetworkDrive');
Route::post('test','App\Http\Controllers\API\Admin\LoginController@test');
Route::any('XTSignNotify','App\Http\Controllers\API\XTSignController@Notify');

@ -17,14 +17,21 @@
return view('welcome');
});
Route::get('/phpstorage/{path}', function ($path) {
// 拼接A服务器的实际路径
$storagePath = 'Z:/' . $path;
$disk = \Illuminate\Support\Facades\Storage::disk('network_drive');
if (!file_exists($storagePath)) {
try {
if (!$disk->exists($path)) {
abort(404);
}
$contents = $disk->get($path);
$mimeType = $disk->mimeType($path);
return response($contents, 200)
->header('Content-Type', $mimeType ?: 'application/octet-stream');
} catch (\Exception $e) {
abort(404);
}
// 返回A服务器的图片
return response()->file($storagePath);
})->where('path', '.*');
//微信登录授权获取code

@ -5,3 +5,8 @@
gin:
# 服务器监听端口
port: "8850"
# 文件存储配置
storage:
# 存储根目录,文件将存储在此目录下
root: "D:/aa"

@ -10,6 +10,7 @@ import (
// Config 应用配置
type Config struct {
Gin GinConfig `json:"gin" yaml:"gin"`
Storage StorageConfig `json:"storage" yaml:"storage"`
}
// GinConfig Gin配置
@ -17,12 +18,20 @@ type GinConfig struct {
Port string `json:"port" yaml:"port"`
}
// StorageConfig 存储配置
type StorageConfig struct {
Root string `json:"root" yaml:"root"`
}
// getDefaultConfig 获取默认配置
func getDefaultConfig() *Config {
return &Config{
Gin: GinConfig{
Port: "8850",
},
Storage: StorageConfig{
Root: "D:/aa",
},
}
}
@ -57,6 +66,11 @@ func loadFromEnv(config *Config) {
if port := os.Getenv("GIN_PORT"); port != "" {
config.Gin.Port = port
}
// 加载 Storage 配置
if root := os.Getenv("STORAGE_ROOT"); root != "" {
config.Storage.Root = root
}
}
// GetConfig 获取配置
@ -70,6 +84,9 @@ func GetConfig() *Config {
if fileConfig.Gin.Port != "" {
config.Gin.Port = fileConfig.Gin.Port
}
if fileConfig.Storage.Root != "" {
config.Storage.Root = fileConfig.Storage.Root
}
}
// 最后从环境变量加载

@ -1,22 +1,67 @@
package router
import (
"encoding/base64"
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"ziqian/config"
u "ziqian/utils"
"github.com/gin-gonic/gin"
)
// getMimeType 根据文件扩展名获取 MIME 类型
func getMimeType(filename string) string {
ext := strings.ToLower(filepath.Ext(filename))
mimeMap := map[string]string{
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
".webp": "image/webp",
".bmp": "image/bmp",
".svg": "image/svg+xml",
".pdf": "application/pdf",
".txt": "text/plain",
".html": "text/html",
".json": "application/json",
".zip": "application/zip",
".doc": "application/msword",
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
".xls": "application/vnd.ms-excel",
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
}
if mime, ok := mimeMap[ext]; ok {
return mime
}
return "application/octet-stream"
}
// fileExists 检查文件是否存在
func fileExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}
// resolveFilePath 将请求路径转换为存储服务器上的绝对路径
func resolveFilePath(storageRoot, requestPath string) string {
// 去除开头的斜杠,避免 filepath.Join 处理异常
cleanPath := strings.TrimPrefix(requestPath, "/")
return filepath.Join(storageRoot, cleanPath)
}
func SetupRouter() *gin.Engine {
r := gin.Default()
cfg := config.GetConfig()
r.LoadHTMLFiles("templates/index.html")
// 只在开发环境中配置静态文件服务
if gin.Mode() == gin.DebugMode {
// 配置静态文件服务
r.Static("/static", "./static")
fmt.Println("Development mode: Static file services enabled")
} else {
@ -117,7 +162,149 @@ func SetupRouter() *gin.Engine {
u.Zi.Echo(c, apiRoutes)
})
// 上传文件 - base64
apiGroup.POST("/upload/base64", func(c *gin.Context) {
var req struct {
File string `json:"file"`
Path string `json:"path"`
Filename string `json:"filename"`
}
if err := c.ShouldBindJSON(&req); err != nil {
u.Zi.Error(c, "请求参数错误")
return
}
if req.File == "" || req.Filename == "" {
u.Zi.Error(c, "文件内容或文件名不能为空")
return
}
// 解码 base64
decoded, err := base64.StdEncoding.DecodeString(req.File)
if err != nil {
u.Zi.Error(c, "base64 解码失败")
return
}
// 构建完整路径
dirPath := resolveFilePath(cfg.Storage.Root, req.Path)
fullPath := filepath.Join(dirPath, req.Filename)
// 创建目录
if err := os.MkdirAll(dirPath, 0755); err != nil {
u.Zi.Error(c, "创建目录失败: "+err.Error())
return
}
// 写入文件
if err := os.WriteFile(fullPath, decoded, 0644); err != nil {
u.Zi.Error(c, "写入文件失败: "+err.Error())
return
}
// 构建返回 URL
relPath := strings.TrimPrefix(req.Path, "/")
url := fmt.Sprintf("/files/%s/%s", relPath, req.Filename)
u.Zi.Echo(c, gin.H{
"url": url,
})
})
// 上传文件 - multipart
apiGroup.POST("/upload/file", func(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
u.Zi.Error(c, "获取上传文件失败")
return
}
path := c.PostForm("path")
if path == "" {
u.Zi.Error(c, "路径不能为空")
return
}
// 构建完整路径
dirPath := resolveFilePath(cfg.Storage.Root, path)
fullPath := filepath.Join(dirPath, file.Filename)
// 创建目录
if err := os.MkdirAll(dirPath, 0755); err != nil {
u.Zi.Error(c, "创建目录失败: "+err.Error())
return
}
// 保存文件
if err := c.SaveUploadedFile(file, fullPath); err != nil {
u.Zi.Error(c, "保存文件失败: "+err.Error())
return
}
// 构建返回 URL
relPath := strings.TrimPrefix(path, "/")
url := fmt.Sprintf("/files/%s/%s", relPath, file.Filename)
u.Zi.Echo(c, gin.H{
"url": url,
})
})
}
// 文件服务路由 - 处理 GET/HEAD/DELETE
r.Any("/files/*filepath", func(c *gin.Context) {
filePath := c.Param("filepath")
// 安全保护:不允许空路径或根路径操作
if filePath == "" || filePath == "/" {
c.Status(http.StatusBadRequest)
return
}
fullPath := resolveFilePath(cfg.Storage.Root, filePath)
// 安全保护:确保解析后的路径仍在存储根目录下
cleanRoot := filepath.Clean(cfg.Storage.Root)
cleanFull := filepath.Clean(fullPath)
if !strings.HasPrefix(cleanFull, cleanRoot) {
c.Status(http.StatusForbidden)
return
}
switch c.Request.Method {
case http.MethodGet:
if !fileExists(fullPath) {
c.Status(http.StatusNotFound)
return
}
c.File(fullPath)
case http.MethodHead:
info, err := os.Stat(fullPath)
if err != nil {
c.Status(http.StatusNotFound)
return
}
c.Header("Content-Type", getMimeType(filePath))
c.Header("Content-Length", fmt.Sprintf("%d", info.Size()))
c.Header("Last-Modified", info.ModTime().Format(http.TimeFormat))
c.Status(http.StatusOK)
case http.MethodDelete:
if err := os.RemoveAll(fullPath); err != nil {
c.Status(http.StatusInternalServerError)
return
}
c.Status(http.StatusOK)
default:
c.Status(http.StatusMethodNotAllowed)
}
})
// 确保文件上传中间件中不包含日志记录(已移除)
// 如果需要将上传的文件写入到磁盘中的指定路径,请使用上述接口
return r
}
Loading…
Cancel
Save