零、目標:
1.專案目標:
A.環境準備
B.建立 Laravel 專案 + 登入系統
C.建立資料庫(資料夾 / 檔案)
D.資料夾 CRUD
E.檔案上傳 / 下載
F.檔案列表(像 Google Drive)
G.分享連結
H.權限與安全
1.專案目標:
A.環境準備
B.建立 Laravel 專案 + 登入系統
C.建立資料庫(資料夾 / 檔案)
D.資料夾 CRUD
E.檔案上傳 / 下載
F.檔案列表(像 Google Drive)
G.分享連結
H.權限與安全
I.回收桶(Soft Delete)
2.目前目標:
G.分享連結(不用登入也能下載)
a.產生公開分享連結
b.未登入也可以下載檔案
c.分享連結使用 token (安全)
d.可設定到期時間(基本版)
2.目前目標:
G.分享連結(不用登入也能下載)
a.產生公開分享連結
b.未登入也可以下載檔案
c.分享連結使用 token (安全)
d.可設定到期時間(基本版)
一、建立 shares 資料夾
1.指令:php artisan make:model Share -m
2.編輯 database/migrations/xxxx_create_shares_table.php
$table->foreignId('file_id')->constrained()->cascadeOnDelete();
$table->string('token')->unique();
$table->timestamp('expires_at')->nullable();
如下圖
二、Share Model
1.編輯 app/Models/Share.php
protected $fillable = [
'file_id',
'token',
'expires_at',
];
public function file(){
return $this->belongsTo(File::class);
}
如下圖
三、修改 File Model (加關聯)
1.編輯 app/Models/File.php
use App\Models\Share;
與
public function share(){
return $this->hasOne(Share::class);
}
如下圖
四、建立 ShareController
1.指令:php artisan make:controller ShareController
2.編輯 app/Http/Controllers/ShareController.php
use App\Models\File;
use App\Models\Share;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Storage;
與
// 建立分享連結
public function create(File $file){
abort_if($file->user_id !== auth()->id(), 403);
$share = Share::firstOrCreate(
['file_id' => $file->id],
['token' => Str::uuid()]
);
return back()->with('share_link', route('share.download', $share->token));
}
// 公開下載
public function download($token){
$share = Share::where('token', $token)->firstOrFail();
$file = $share->file;
return Storage::disk('public')->download($file->path, $file->name);
}
如下圖
五、設定路由
1.編輯 routes/web.php
// Share
Route::post('/files/{file}/share', [ShareController::class, 'create'])
->name('files.share');
與
// Share
Route::get('/share/{token}', [ShareController::class, 'download'])
->name('share.download');
如下圖
六、修改檔案列表畫面(加入分享按鈕)
1.編輯 folders/show.blade.php
{{-- 檔案列表 --}}
<h2 class="font-semibold mb-2">檔案</h2>
<ul class="space-y-2">
@forelse($files as $file)
<li class="border p-3 rounded flex justify-between items-center">
📄 {{ $file->name }}
<div class="space-x-3">
<a href="{{ route('files.download', $file) }}"
class="text-blue-600 hover:underline">
下載
</a>
<form method="POST" action="{{ route('files.share', $file) }}" class="inline">
@csrf
<button class="text-green-600 hover:underline">
分享
</button>
</form>
</div>
</li>
@empty
<li class="text-gray-400">尚無檔案</li>
@endforelse
</ul>
{{-- 顯示分享連結 --}}
@if(session('share_link'))
<div class="mt-4 p-3 bg-green-100 border rounded">
分享連結:
<a href="{{ session('share_link') }}"
class="text-blue-600 underline"
target="_blank">
{{ session('share_link') }}
</a>
</div>
@endif
如下圖
七、測試流程
1.點進資料夾
2.上傳檔案
3.點[分享]
4.複製連結
5.登出或開無痕視窗
6.貼上連結->成功下載
<x-app-layout>
<div class="p-6 max-w-4xl mx-auto">
{{-- Breadcrumb --}}
<div class="mb-4 text-sm text-gray-500">
我的雲端硬碟
</div>
<h1 class="text-xl font-bold mb-4">我的資料夾</h1>
{{-- 成功訊息 --}}
@if(session('success'))
<div class="mb-4 text-green-600">
{{ session('success') }}
</div>
@endif
{{-- 新增根資料夾 --}}
<form method="POST" action="{{ route('folders.store') }}" class="mb-4 flex gap-2">
@csrf
<input type="text" name="name" placeholder="新資料夾名稱" class="border rounded p-2 flex-1" required>
<button class="bg-blue-500 text-white px-4 py-2 rounded">新增</button>
</form>
{{-- 根目錄資料夾列表 --}}
<ul class="space-y-2 mb-6">
@forelse($folders as $folder)
<li class="border p-3 rounded hover:bg-gray-100 flex justify-between items-center"
x-data="{ editing: false, name: '{{ e($folder->name) }}' }">
{{-- 資料夾名稱 --}}
<div class="flex items-center gap-2">
📁
<template x-if="!editing">
<span @dblclick="editing = true" class="font-semibold text-blue-600 cursor-pointer">
{{ $folder->name }}
</span>
</template>
<template x-if="editing">
<form method="POST" action="{{ route('folders.update', $folder) }}" class="flex items-center gap-1">
@csrf
@method('PATCH')
<input type="text" name="name" x-model="name"
@keydown.enter.prevent="$el.form.submit()"
@keydown.escape="editing = false"
class="border rounded p-1 text-sm w-32" autofocus required>
<button class="text-green-600 text-sm">儲存</button>
<button type="button" @click="editing = false" class="text-gray-500 text-sm">取消</button>
</form>
</template>
</div>
{{-- 操作按鈕 --}}
<div class="flex gap-2 items-center">
<a href="{{ route('folders.show', $folder) }}" class="text-sm text-blue-500">開啟</a>
<button type="button" @click="editing = true" class="text-sm text-gray-600">重新命名</button>
<form method="POST" action="{{ route('folders.destroy', $folder) }}"
onsubmit="return confirm('確定要刪除這個資料夾及其所有子資料夾嗎?')">
@csrf
@method('DELETE')
<button class="text-sm text-red-600">刪除</button>
</form>
</div>
</li>
@empty
<li class="text-gray-400">尚無資料夾</li>
@endforelse
</ul>
{{-- 多檔案上傳 --}}
<form method="POST" action="{{ route('files.store') }}" enctype="multipart/form-data" class="mb-4 flex gap-2">
@csrf
<input type="hidden" name="folder_id" value="">
<input type="file" name="files[]" multiple required>
<button class="bg-green-600 text-white px-4 py-2 rounded">上傳檔案</button>
</form>
{{-- 檔案排序 --}}
<div class="mb-2 flex items-center gap-2 text-sm text-gray-500">
排序:
@php
$options = ['name' => '名稱', 'size' => '大小', 'created_at' => '上傳日期'];
@endphp
@foreach($options as $key => $label)
<a href="{{ route('folders.index') }}?sort={{ $key }}&direction={{ $sort === $key && $direction === 'asc' ? 'desc' : 'asc' }}"
class="hover:underline {{ $sort === $key ? 'font-semibold' : '' }}">
{{ $label }}
@if($sort === $key)
{{ $direction === 'asc' ? '↑' : '↓' }}
@endif
</a>
@if(!$loop->last) | @endif
@endforeach
</div>
{{-- 根目錄檔案列表 --}}
<h2 class="font-semibold mb-2">檔案</h2>
<ul class="space-y-2">
@forelse($files as $file)
<li class="border p-3 rounded flex justify-between items-center"
x-data="{ editing: false, name: '{{ e($file->name) }}' }">
{{-- 檔案名稱 + 大小 + 上傳日期 --}}
<div class="flex items-center gap-4">
📄
<template x-if="!editing">
<span @dblclick="editing = true" class="cursor-pointer text-blue-600">
{{ $file->name }}
</span>
</template>
<template x-if="editing">
<form method="POST" action="{{ route('files.update', $file) }}" class="flex items-center gap-1">
@csrf
@method('PATCH')
<input type="text" name="name" x-model="name"
@keydown.enter.prevent="$el.form.submit()"
@keydown.escape="editing = false"
class="border rounded p-1 text-sm w-32" autofocus required>
<button class="text-green-600 text-sm">儲存</button>
<button type="button" @click="editing = false" class="text-gray-500 text-sm">取消</button>
</form>
</template>
{{-- 檔案大小 --}}
<span class="text-gray-500 text-sm">
@php
if($file->size >= 1048576){
$size = number_format($file->size / 1048576, 2) . ' MB';
} elseif($file->size >= 1024){
$size = number_format($file->size / 1024, 2) . ' KB';
} else {
$size = $file->size . ' B';
}
@endphp
{{ $size }}
</span>
{{-- 上傳日期 --}}
<span class="text-gray-400 text-sm">{{ $file->created_at->format('Y-m-d H:i') }}</span>
</div>
{{-- 操作按鈕 --}}
<div class="flex justify-center gap-2 items-center">
<a href="{{ route('files.download', $file) }}" class="text-blue-600 hover:underline">下載</a>
<button type="button" @click="editing = true" class="text-sm text-gray-600">重新命名</button>
<form method="POST" action="{{ route('files.destroy', $file) }}" onsubmit="return confirm('確定要刪除這個檔案嗎?')">
@csrf
@method('DELETE')
<button class="text-red-600 text-sm">刪除</button>
</form>
</div>
{{-- 分享按鈕靠右 --}}
<div class="flex justify-end gap-2 items-center mt-2">
<form method="POST" action="{{ route('files.share', $file) }}" class="inline">
@csrf
<button type="submit" class="text-blue-600 text-sm">分享</button>
</form>
{{-- 顯示分享連結 --}}
@if(session('share_link_' . $file->id))
<span class="text-gray-500 text-sm ml-2">分享連結:
<a href="{{ session('share_link_' . $file->id) }}" target="_blank" class="text-blue-600">{{ session('share_link_' . $file->id) }}</a>
</span>
@endif
</div>
</li>
@empty
<li class="text-gray-400">尚無檔案</li>
@endforelse
</ul>
</div>
</x-app-layout>
2.編輯 folders/show.blade.php
<x-app-layout>
<div class="p-6 max-w-4xl mx-auto">
{{-- Breadcrumb --}}
<div class="mb-4 text-sm text-gray-500">
<a href="{{ route('folders.index') }}" class="hover:underline">我的雲端硬碟</a>
@foreach ($breadcrumbs as $crumb)
/ <a href="{{ route('folders.show', $crumb) }}" class="hover:underline">{{ $crumb->name }}</a>
@endforeach
</div>
<h1 class="text-xl font-bold mb-4">📁 {{ $folder->name }}</h1>
{{-- 成功訊息 --}}
@if(session('success'))
<div class="mb-4 text-green-600">
{{ session('success') }}
</div>
@endif
{{-- 子資料夾列表 --}}
@if($subFolders->isNotEmpty())
<h2 class="font-semibold mb-2">子資料夾</h2>
<ul class="space-y-2 mb-6">
@foreach($subFolders as $subFolder)
<li class="border p-3 rounded hover:bg-gray-100 flex justify-between items-center">
<div class="flex items-center gap-2">
📁
<a href="{{ route('folders.show', $subFolder) }}" class="text-blue-600">
{{ $subFolder->name }}
</a>
</div>
{{-- 子資料夾操作按鈕 --}}
<div class="flex gap-2 items-center">
<a href="{{ route('folders.show', $subFolder) }}" class="text-sm text-blue-500">開啟</a>
<button type="button" class="text-sm text-gray-600">重新命名</button>
<form method="POST" action="{{ route('folders.destroy', $subFolder) }}"
onsubmit="return confirm('確定要刪除這個資料夾及其所有子資料夾嗎?')">
@csrf
@method('DELETE')
<button class="text-sm text-red-600">刪除</button>
</form>
</div>
</li>
@endforeach
</ul>
@else
<p class="text-gray-400">此資料夾下沒有子資料夾。</p>
@endif
{{-- 檔案排序 --}}
<div class="mb-2 flex items-center gap-2 text-sm text-gray-500">
排序:
@php
$options = ['name' => '名稱', 'size' => '大小', 'created_at' => '上傳日期'];
@endphp
@foreach($options as $key => $label)
<a href="{{ route('folders.show', $folder) }}?sort={{ $key }}&direction={{ $sort === $key && $direction === 'asc' ? 'desc' : 'asc' }}"
class="hover:underline {{ $sort === $key ? 'font-semibold' : '' }}">
{{ $label }}
@if($sort === $key)
{{ $direction === 'asc' ? '↑' : '↓' }}
@endif
</a>
@if(!$loop->last) | @endif
@endforeach
</div>
{{-- 檔案列表 --}}
<h2 class="font-semibold mb-2">檔案</h2>
<ul class="space-y-2">
@forelse($files as $file)
<li class="border p-3 rounded flex justify-between items-center"
x-data="{ editing: false, name: '{{ e($file->name) }}' }">
{{-- 檔案名稱 + 大小 + 上傳日期 --}}
<div class="flex items-center gap-4">
📄
<template x-if="!editing">
<span @dblclick="editing = true" class="cursor-pointer text-blue-600">
{{ $file->name }}
</span>
</template>
<template x-if="editing">
<form method="POST" action="{{ route('files.update', $file) }}" class="flex items-center gap-1">
@csrf
@method('PATCH')
<input type="text" name="name" x-model="name"
@keydown.enter.prevent="$el.form.submit()"
@keydown.escape="editing = false"
class="border rounded p-1 text-sm w-32" autofocus required>
<button class="text-green-600 text-sm">儲存</button>
<button type="button" @click="editing = false" class="text-gray-500 text-sm">取消</button>
</form>
</template>
{{-- 檔案大小 --}}
<span class="text-gray-500 text-sm">
@php
if($file->size >= 1048576){
$size = number_format($file->size / 1048576, 2) . ' MB';
} elseif($file->size >= 1024){
$size = number_format($file->size / 1024, 2) . ' KB';
} else {
$size = $file->size . ' B';
}
@endphp
{{ $size }}
</span>
{{-- 上傳日期 --}}
<span class="text-gray-400 text-sm">{{ $file->created_at->format('Y-m-d H:i') }}</span>
</div>
{{-- 操作按鈕 --}}
<div class="flex gap-2 items-center">
<a href="{{ route('files.download', $file) }}" class="text-blue-600 hover:underline">下載</a>
<button type="button" @click="editing = true" class="text-sm text-gray-600">重新命名</button>
<form method="POST" action="{{ route('files.destroy', $file) }}"
onsubmit="return confirm('確定要刪除這個檔案嗎?')">
@csrf
@method('DELETE')
<button class="text-red-600 text-sm">刪除</button>
</form>
</div>
{{-- 分享連結 --}}
<div class="flex gap-2 items-center mt-2">
<form method="POST" action="{{ route('files.share', $file) }}" class="inline">
@csrf
<button type="submit" class="text-blue-600 text-sm">分享</button>
</form>
{{-- 顯示分享連結 --}}
@if(session('share_link_' . $file->id))
<span class="text-gray-500 text-sm ml-2">分享連結:
<a href="{{ session('share_link_' . $file->id) }}" target="_blank" class="text-blue-600">{{ session('share_link_' . $file->id) }}</a>
</span>
@endif
</div>
</li>
@empty
<li class="text-gray-400">尚無檔案</li>
@endforelse
</ul>
</div>
</x-app-layout>
3.結果如下:









沒有留言:
張貼留言