Skip to content

Instantly share code, notes, and snippets.

@zisan34
Last active March 10, 2021 04:59
Show Gist options
  • Save zisan34/b12749285c48c2f19f1f5e1bbef74685 to your computer and use it in GitHub Desktop.
Save zisan34/b12749285c48c2f19f1f5e1bbef74685 to your computer and use it in GitHub Desktop.
Vue-Laravel Large File Upload by Chunk Upload.
<?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]);
}
}
<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>
<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