Last active
March 10, 2021 04:59
-
-
Save zisan34/b12749285c48c2f19f1f5e1bbef74685 to your computer and use it in GitHub Desktop.
Vue-Laravel Large File Upload by Chunk Upload.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Http\Controllers; | |
use Illuminate\Http\Request; | |
use Illuminate\Support\Facades\Storage; | |
class ChunkUploadController extends Controller | |
{ | |
private function storage() | |
{ | |
return Storage::disk('public'); | |
} | |
public function uploadChunk(Request $request) | |
{ | |
$this->validate($request, [ | |
'file_unique_key' => 'required|string', | |
'chunk_number' => 'required_if:ongoing,=,true|numeric', | |
'file' => 'required_if:ongoing,=,true|file|max:10000', | |
'cancelled' => 'nullable|boolean' | |
]); | |
// if($request->chunk_number > 5){ | |
// abort(403); | |
// } | |
$directory = "/file_explorer/chunks/{$request->file_unique_key}"; | |
if ($request->boolean('cancelled') == true) { | |
if ($this->storage()->exists($directory)) { | |
$this->storage()->deleteDirectory($directory); | |
} | |
return response()->json([ | |
'cancelled' => true | |
]); | |
} | |
$file = $request->file('file'); | |
if (!$this->storage()->exists($directory)) { | |
$this->storage()->makeDirectory($directory); | |
} | |
$file_org_name = $file->getClientOriginalName(); | |
$part_file_name_dir = "{$directory}/{$file->getClientOriginalName()}.{$request->chunk_number}"; | |
$this->storage()->putFileAs("", $file, $part_file_name_dir); | |
if ($request->has('is_last') && $request->boolean('is_last')) { | |
$final_directory = "/file_explorer/uploads/" . time() . '_' . rand() . '_' . auth()->id(); | |
if (!$this->storage()->exists($final_directory)) { | |
$this->storage()->makeDirectory($final_directory); | |
} | |
$all_chunked_files = $this->storage()->allFiles($directory); | |
foreach ($all_chunked_files as $key => $chunked_file) { | |
// **** this one uses file_put_contents($path, $data, FILE_APPEND) which reads the whole file then appends to it. which causes memory limit exceeded issue. **** | |
// $this->storage()->append($final_directory . '/' . $file_org_name, $this->storage()->get($chunked_file)); | |
// *** this one just appends to the file. so no memory issue *** | |
$file_handler = fopen('file://' . $this->storage()->path($final_directory . '/' . $file_org_name), 'a'); | |
fputs($file_handler, $this->storage()->get($chunked_file)); | |
fclose($file_handler); | |
} | |
// $this->storage()->move($part_file_name_dir, $final_directory . '/' . $file_org_name); | |
$this->storage()->deleteDirectory($directory); | |
return response()->json([ | |
'uploaded' => true, | |
'uploaded_file_path' => "{$final_directory}/{$file_org_name}" | |
]); | |
} | |
return response()->json(['uploaded' => true]); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<template> | |
<div> | |
<div v-if="progress > 0"> | |
<div class="progress"> | |
<div | |
class="progress-bar" | |
role="progressbar" | |
:aria-valuenow="progress" | |
aria-valuemin="0" | |
aria-valuemax="100" | |
:style="`width:${progress}%`" | |
> | |
<span class="sr-only">{{ progress }}% Complete</span> | |
</div> | |
</div> | |
</div> | |
<div class="pt-2"> | |
<input type="file" @change="select" /> | |
</div> | |
</div> | |
</template> | |
<script> | |
export default { | |
props: ["url"], | |
data() { | |
return { | |
file: null, | |
chunk_size: 5*1024*1024, | |
chunks: [], | |
failed_chunks: [], | |
file_unique_key: "", | |
uploaded_file_path: "", | |
}; | |
}, | |
beforeDestroy() { | |
if (this.progress > 0 && this.progress < 100) { | |
alert("Uploading Cancelled."); | |
this.sendCancelRequest(); | |
} | |
}, | |
computed: { | |
progress() { | |
return !!this.file | |
? ((this.chunks_count - this.chunks.length) * 100) / this.chunks_count | |
: 0; | |
}, | |
formData() { | |
let formData = new FormData(); | |
formData.set("ongoing", true); | |
formData.set("file_unique_key", this.file_unique_key); | |
formData.set("is_last", this.chunks.length === 1); | |
formData.set("chunk_number", this.chunks[0].chunk_number); | |
formData.set("file", this.chunks[0].file, this.file.name); | |
return formData; | |
}, | |
config() { | |
return { | |
method: "POST", | |
data: this.formData, | |
url: this.url, | |
headers: { | |
"Content-Type": "application/octet-stream", | |
}, | |
}; | |
}, | |
chunks_count() { | |
return this.file ? Math.ceil(this.file.size / this.chunk_size) : 0; | |
}, | |
}, | |
methods: { | |
sendCancelRequest(){ | |
axios.post(this.url, { | |
file_unique_key: this.file_unique_key, | |
cancelled: true, | |
}); | |
}, | |
resetState() { | |
this.file = null; | |
this.file_unique_key = ""; | |
this.uploaded_file_path = ""; | |
this.chunks.splice(0, this.chunks.length); | |
this.failed_chunks.splice(0, this.failed_chunks.length); | |
}, | |
select(event) { | |
this.$emit('updateFilePath', ""); | |
if(this.file_unique_key != ""){ | |
this.sendCancelRequest(); | |
} | |
this.resetState(); | |
this.file = event.target.files.item(0); | |
if (this.file) { | |
this.$emit('updateFileTitle', this.file.name); | |
this.file_unique_key = Math.random().toString(36).substring(3); | |
this.createChunks(); | |
} | |
}, | |
upload() { | |
axios(this.config) | |
.then((response) => { | |
this.chunks.shift(); | |
if (response.data.uploaded_file_path) { | |
this.uploaded_file_path = response.data.uploaded_file_path; | |
this.$emit('updateFilePath', this.uploaded_file_path); | |
} | |
}) | |
.catch((error) => { | |
// this.failed_chunks.push(this.chunks.shift()); //needs work | |
alert('File upload failed, please try again.'); | |
this.sendCancelRequest(); | |
this.resetState(); | |
}); | |
}, | |
createChunks() { | |
for (let i = 0; i < this.chunks_count; i++) { | |
this.chunks.push({ | |
chunk_number: i + 1, | |
file: this.file.slice( | |
i * this.chunk_size, | |
Math.min(i * this.chunk_size + this.chunk_size, this.file.size), | |
this.file.type | |
), | |
}); | |
} | |
}, | |
}, | |
watch: { | |
chunks(n, o) { | |
if (n.length > 0) { | |
this.upload(); | |
} | |
}, | |
}, | |
}; | |
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<template> | |
<div class="row"> | |
<div class="col-lg-12"> | |
<div class="card"> | |
<div class="card-body"> | |
<div class="mb-3"> | |
<span class="h3">{{ edit_mode ? "Edit file" : "Add file" }}</span> | |
<button | |
class="btn btn-danger float-right" | |
@click.prevent="$emit('close')" | |
> | |
x | |
</button> | |
</div> | |
<form @submit.prevent="submitForm"> | |
<div class="form-group"> | |
<label for="title">File Title</label> | |
<input | |
type="text" | |
class="form-control" | |
v-model="file_form.file_title" | |
/> | |
</div> | |
<div class="form-group" v-if="!edit_mode"> | |
<label class="w-100"> | |
<ChunkUploader | |
url="/file-explorer/chunk-file-upload" | |
@updateFileTitle="updateFileTitle" | |
@updateFilePath="updateFilePath" | |
/> | |
</label> | |
</div> | |
<div class="form-group"> | |
<button | |
type="submit" | |
:disabled="!edit_mode && file_form.file_path == ''" | |
class="btn btn-primary waves-effect waves-light" | |
> | |
Submit | |
</button> | |
<button class="btn btn-secondary" @click.prevent="$emit('close')"> | |
Cancel | |
</button> | |
</div> | |
</form> | |
</div> | |
</div> | |
</div> | |
</div> | |
</template> | |
<script> | |
import ChunkUploader from "@/components/Shared/FileExplorer/ChunkUploader"; | |
export default { | |
props: ["file_form", "current_element_id", "edit_mode"], | |
created: function(){ | |
if(!this.file_form.file_path){ | |
this.$set(this.file_form, 'file_path', ""); | |
} | |
}, | |
methods: { | |
updateFilePath(file_path) { | |
this.file_form.file_path = file_path; | |
}, | |
updateFileTitle(file_title) { | |
this.file_form.file_title = this.file_form.file_title || file_title; | |
}, | |
submitForm() { | |
this.$page.errors = {}; | |
axios({ | |
method: this.edit_mode ? "put" : "post", | |
url: `/file-explorer/${this.edit_mode ? this.file_form.id : ""}`, | |
data: { | |
current_element_id: this.current_element_id, | |
element_type: "file", | |
file: { | |
file_title: this.file_form.file_title, | |
file_path: this.file_form.file_path, | |
}, | |
}, | |
headers: { "Block-UI": true }, | |
}) | |
.then((response) => { | |
if (response.status == 200 && response.data.type == "success") { | |
this.$emit("close"); | |
this.$inertia.visit(response.data.redirect_to).then(() => { | |
window.Toast.fire({ | |
icon: "success", | |
title: response.data.message, | |
}); | |
}); | |
} | |
}) | |
.catch((error) => { | |
this.$page.errors = error.response.data.errors; | |
}); | |
}, | |
}, | |
components: { ChunkUploader }, | |
}; | |
</script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment