@@ -26,14 +26,18 @@ import GuestLayout from '@/Layouts/GuestLayout.vue';
mais aussi à faire des bricolages, constructions et autres projets en fonction de leur âge.
Plein d’autres activités sont également au programme :
-
- - Jeux de rôle
- - Marches
- - Bricolage
- - Théâtre
- - Chant
- - Etc.
-
+
+
+ - Jeux de rôle
+ - Marches
+ - Bricolage
+
+
+ - Théâtre
+ - Chant
+ - Etc.
+
+
Tout ce qui va permette à chacun de se développer, de se connaître.
Et tous les anciens vous le dirons, la solidarité est telle que les groupes de scout vous donnent surtout les meilleurs amis pour la vie.
diff --git a/resources/js/Pages/Photo/Create.vue b/resources/js/Pages/Photo/Create.vue
deleted file mode 100644
index e69de29..0000000
diff --git a/resources/js/Pages/Photo/Index.vue b/resources/js/Pages/Photo/Index.vue
index e69de29..2051fb4 100644
--- a/resources/js/Pages/Photo/Index.vue
+++ b/resources/js/Pages/Photo/Index.vue
@@ -0,0 +1,81 @@
+
+
+
+
+
+ { fullScreenState.photo = {}; fullScreenState.active = false; }"
+ />
+
+
+
+
+
Affichage
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/resources/js/Pages/Photo/Partials/Create.vue b/resources/js/Pages/Photo/Partials/Create.vue
new file mode 100644
index 0000000..a62d75b
--- /dev/null
+++ b/resources/js/Pages/Photo/Partials/Create.vue
@@ -0,0 +1,81 @@
+
+
+
+
+
+
![]()
+
+
+
emits('close')" class="w-full flex justify-end ">
+

+
+
+
+
+
\ No newline at end of file
diff --git a/resources/js/Pages/Photo/Partials/Edit.vue b/resources/js/Pages/Photo/Partials/Edit.vue
new file mode 100644
index 0000000..1cb55d2
--- /dev/null
+++ b/resources/js/Pages/Photo/Partials/Edit.vue
@@ -0,0 +1,40 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/resources/js/Pages/Photo/Partials/Modal.vue b/resources/js/Pages/Photo/Partials/Modal.vue
new file mode 100644
index 0000000..b1db072
--- /dev/null
+++ b/resources/js/Pages/Photo/Partials/Modal.vue
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
![]()
+
+
+
+
+
+
\ No newline at end of file
diff --git a/resources/js/Pages/Photo/Partials/Show.vue b/resources/js/Pages/Photo/Partials/Show.vue
new file mode 100644
index 0000000..64a452f
--- /dev/null
+++ b/resources/js/Pages/Photo/Partials/Show.vue
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
photoState.edit = false"
+ :photo="props.photo"
+ :class="'absolute left-0 top-full mt-2'"
+ />
+
+
+
+
+
{{ props.photo.name }}
+
publier par {{ props.photo.user.name }}
+
+
+
+

+
+
![]()
+
+
\ No newline at end of file
diff --git a/resources/js/Pages/Photo/Show.vue b/resources/js/Pages/Photo/Show.vue
deleted file mode 100644
index e69de29..0000000
diff --git a/resources/js/file.js b/resources/js/file.js
new file mode 100644
index 0000000..e9f4b8a
--- /dev/null
+++ b/resources/js/file.js
@@ -0,0 +1,50 @@
+export default class File {
+ static SizeToString(size)
+ {
+ let cpt = 0;
+ let units = ["octet", "Ko", "Mo", "Go", "To"];
+ while(size >= 1000 && size >= 1)
+ {
+ size = size / 1000;
+ cpt++;
+ }
+ return (Math.ceil(size * 100) / 100) + " " + units[cpt];
+ }
+
+ static Icon(ext)
+ {
+ let img = "/icons/fichier.png";
+ switch(ext.toLowerCase()) {
+ case 'pdf': return "/icons/pdf.png";
+ case 'dwg': return "/icons/dwg.png";
+ case 'stp':
+ case 'step': return "/icons/step.png";
+ case 'xlsx': return "/icons/excel.png";
+ case 'docx': return "/icons/word.png";
+ case 'zip':
+ case '7z':
+ case 'rar':
+ case 'gz': return "/icons/zip.png";
+ case "unitypackage": return "/icons/unity.svg";
+ case "png":
+ case "jfif":
+ case "jpeg":
+ case "jpg":
+ case "gif":
+ case "svg":
+ case "webp": return "/icons/photo.svg";
+ }
+ return img;
+ }
+
+ static Basename(filename)
+ {
+ return filename.substring(filename.lastIndexOf('/') > 0 ? filename.lastIndexOf('/') + 1 : 0, filename.lastIndexOf('.')) || filename;
+ }
+
+ static Extension(filename)
+ {
+ return filename.substring(filename.lastIndexOf('.')+1, filename.length) || filename;
+ }
+
+}
diff --git a/resources/js/storageS3.js b/resources/js/storageS3.js
new file mode 100644
index 0000000..4d1cf7a
--- /dev/null
+++ b/resources/js/storageS3.js
@@ -0,0 +1,106 @@
+export default class StorageS3
+{
+ static options =
+ {
+ genUrl: "/s3/generate-presigned-url",
+ startMultipartUrl: "/s3/start-multipart-upload",
+ genMultipartUrl: "/s3/generate-presigned-multipart-url",
+ completeMultipartUrl: "/s3/complete-multipart-upload",
+ proxyMultipartUrl: "/s3/proxy-multipart-upload",
+ partSize: 32 * 1024 * 1024,
+ }
+
+ static async UploadFile(file, callbacks = {
+ onUpdate: async (done) => {},
+ onSuccess: async () => {},
+ onError: async () => {}
+ })
+ {
+ try {
+ const uploadPromises = [];
+ let data = new Uint8Array(await file.arrayBuffer());
+ const { uploadId, key } = await StorageS3.StartMultiPartUpload(file.name);
+ let partIndex = 0;
+ for (let start = 0; start < file.size; start += StorageS3.options.partSize)
+ {
+ partIndex++;
+ let partNumber = partIndex;
+ let length = start + StorageS3.options.partSize < file.size ? start + StorageS3.options.partSize : file.size;
+ if(callbacks.onUpdate) await callbacks.onUpdate((length/file.size) * 100);
+ await new Promise(r => setTimeout(r, 200));
+ const partData = data.subarray(start, length);
+ uploadPromises.push(
+ StorageS3.GenerateMultipartSignedUrl(key, uploadId, partNumber, partData.length)
+ .then(({ url }) => StorageS3.UploadPart(url, partData, partNumber))
+ .then(({ ETag }) => ({ ETag, PartNumber: partNumber }))
+ );
+ }
+ let parts = await Promise.all(uploadPromises);
+ parts = parts.sort((a, b) => a.PartNumber - b.PartNumber);
+ await StorageS3.CompleteMultiPartUpload(key, uploadId, parts);
+ if(callbacks.onSuccess) await callbacks.onSuccess(key);
+ } catch(e) { if(callbacks.onError) await callbacks.onError(e); }
+ }
+
+ static async StartMultiPartUpload(key)
+ {
+ const response = await fetch(`${StorageS3.options.startMultipartUrl}?filename=${encodeURIComponent(key)}`, {
+ method: "GET",
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+ return response.json();
+ }
+
+ static async GenerateMultipartSignedUrl(key, uploadId, partNumber, partLength)
+ {
+ const response = await fetch(`${StorageS3.options.genMultipartUrl}?key=${encodeURIComponent(key)}&uploadId=${uploadId}&partNumber=${partNumber}&partLength=${partLength}`, {
+ method: "GET",
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+ return response.json();
+ }
+
+ static async GenerateSignedUrl(key)
+ {
+ const response = await fetch(`${StorageS3.options.genUrl}?key=${encodeURIComponent(key)}`, { method: "GET" });
+ return response.json();
+ }
+
+ static async UploadPart(signedUrl, partData, partNumber)
+ {
+ const response = await fetch(`${StorageS3.options.proxyMultipartUrl}`, {
+ method: 'PUT',
+ headers: {
+ "X-CSRF-Token": document.querySelector('input[name=_token]').value,
+ "X-SignedUrl": signedUrl,
+ "Content-Length": partData.length,
+ },
+ body: partData,
+ });
+ if (!response.ok) {
+ throw new Error(`Failed to upload part: ${partNumber}`);
+ }
+ return response.json(); // Returns ETag
+ }
+
+ static async CompleteMultiPartUpload(key, uploadId, parts)
+ {
+ const response = await fetch(StorageS3.options.completeMultipartUrl, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ "X-CSRF-Token": document.querySelector('input[name=_token]').value,
+ },
+ body: JSON.stringify({
+ key,
+ uploadId,
+ parts,
+ }),
+ });
+ return response.json();
+ }
+}
diff --git a/resources/js/utils.js b/resources/js/utils.js
new file mode 100644
index 0000000..b5d47de
--- /dev/null
+++ b/resources/js/utils.js
@@ -0,0 +1,6 @@
+export default class Utils {
+ static Prevent(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ }
+}
diff --git a/resources/views/app.blade.php b/resources/views/app.blade.php
index 951cd48..a71911b 100644
--- a/resources/views/app.blade.php
+++ b/resources/views/app.blade.php
@@ -18,6 +18,7 @@
@inertiaHead
+ @csrf
@inertia
diff --git a/routes/web.php b/routes/web.php
index 69ced90..c6673cb 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -5,14 +5,10 @@
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
+use App\Http\Controllers\S3Controller;
Route::get('/', function () {
- return Inertia::render('Home', [
- 'canLogin' => Route::has('login'),
- 'canRegister' => Route::has('register'),
- 'laravelVersion' => Application::VERSION,
- 'phpVersion' => PHP_VERSION,
- ]);
+ return Inertia::render('Home');
})->name('home');
Route::get('/info', function () {
@@ -20,17 +16,23 @@
})->name('info');
Route::get('/photos', [PhotoController::class, 'index'])->name('photo.index');
-Route::get('/photo/{id}', [ProfileController::class, 'show'])->name('photo.show');
-Route::post('/photo/{id}/edit', [ProfileController::class, 'store'])->name('photo.edit');
-Route::post('/photo/{id}', [ProfileController::class, 'store'])->name('photo.store');
-Route::patch('/photo/{id}', [ProfileController::class, 'update'])->name('photo.update');
-Route::delete('/photo/{id}', [ProfileController::class, 'destroy'])->name('photo.destroy');
+Route::post('/photo', [PhotoController::class, 'store'])->name('photo.store');
+Route::post('/photo/{id}', [PhotoController::class, 'update'])->name('photo.update');
+Route::delete('/photo/{id}', [PhotoController::class, 'destroy'])->name('photo.destroy');
-Route::get('/dashboard', function () {
- return Inertia::render('Dashboard');
-})->middleware(['auth', 'verified'])->name('dashboard');
+
+Route::get("/s3/start-multipart-upload", [S3Controller::class, "StartMultipartUpload"])->name("s3.multipart.start");
+Route::get("/s3/generate-presigned-multipart-url", [S3Controller::class, "GeneratePresignedMultipartUrl"])->name("s3.multipart.presigned-url");
+Route::post("/s3/complete-multipart-upload", [S3Controller::class, "CompleteMultipartUpload"])->name("s3.multipart.complete");
+Route::get("/s3/generate-presigned-url", [S3Controller::class, "GeneratePresignedUrl"])->name("s3.presigned-url");
+Route::put("/s3/proxy-multipart-upload", [S3Controller::class, "ProxyS3"])->name("s3.proxy");
+Route::get("/s3/download", [S3Controller::class, "Download"])->name("s3.download");
Route::middleware(['auth', 'verified'])->group(function () {
+ Route::get('/dashboard', function () {
+ return Inertia::render('Dashboard');
+ })->name('dashboard');
+
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');