mirror of
https://github.com/Fabio286/antares.git
synced 2025-06-05 21:59:22 +02:00
feat: blob fields edit/view/download
This commit is contained in:
@ -10,8 +10,8 @@ const regex = new RegExp(/[\0\x08\x09\x1a\n\r"'\\\%]/g);
|
|||||||
*/
|
*/
|
||||||
function sqlEscaper (string) {
|
function sqlEscaper (string) {
|
||||||
return string.replace(regex, (char) => {
|
return string.replace(regex, (char) => {
|
||||||
var m = ['\\0', '\\x08', '\\x09', '\\x1a', '\\n', '\\r', '\'', '"', '\\', '\\\\', '%'];
|
const m = ['\\0', '\\x08', '\\x09', '\\x1a', '\\n', '\\r', '\'', '"', '\\', '\\\\', '%'];
|
||||||
var r = ['\\\\0', '\\\\b', '\\\\t', '\\\\z', '\\\\n', '\\\\r', '\'\'', '""', '\\\\', '\\\\\\\\', '\\%'];
|
const r = ['\\\\0', '\\\\b', '\\\\t', '\\\\z', '\\\\n', '\\\\r', '\'\'', '""', '\\\\', '\\\\\\\\', '\\%'];
|
||||||
return r[m.indexOf(char)];
|
return r[m.indexOf(char)];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ export function mimeFromHex (hex) {
|
|||||||
case '424D':
|
case '424D':
|
||||||
return { ext: 'bmp', mime: 'image/bmp' };
|
return { ext: 'bmp', mime: 'image/bmp' };
|
||||||
case '1F8B':
|
case '1F8B':
|
||||||
return { ext: 'gz', mime: 'application/gzip' };
|
return { ext: 'tar.gz', mime: 'application/gzip' };
|
||||||
case '0B77':
|
case '0B77':
|
||||||
return { ext: 'ac3', mime: 'audio/vnd.dolby.dd-raw' };
|
return { ext: 'ac3', mime: 'audio/vnd.dolby.dd-raw' };
|
||||||
case '7801':
|
case '7801':
|
||||||
@ -20,7 +20,7 @@ export function mimeFromHex (hex) {
|
|||||||
default:
|
default:
|
||||||
switch (hex.substring(0, 6)) { // 3 bytes
|
switch (hex.substring(0, 6)) { // 3 bytes
|
||||||
case 'FFD8FF':
|
case 'FFD8FF':
|
||||||
return { ext: 'jpj', mime: 'image/jpeg' };
|
return { ext: 'jpg', mime: 'image/jpeg' };
|
||||||
case '4949BC':
|
case '4949BC':
|
||||||
return { ext: 'jxr', mime: 'image/vnd.ms-photo' };
|
return { ext: 'jxr', mime: 'image/vnd.ms-photo' };
|
||||||
case '425A68':
|
case '425A68':
|
||||||
@ -40,7 +40,7 @@ export function mimeFromHex (hex) {
|
|||||||
case '4D4D002A':
|
case '4D4D002A':
|
||||||
return { ext: 'tif', mime: 'image/tiff' };
|
return { ext: 'tif', mime: 'image/tiff' };
|
||||||
default:
|
default:
|
||||||
return { ext: '???', mime: 'unknown ' + hex };
|
return { ext: '', mime: 'unknown ' + hex };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -57,3 +57,10 @@ export function formatBytes (bytes, decimals = 2) {
|
|||||||
|
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function bufferToBase64 (buf) {
|
||||||
|
const binstr = Array.prototype.map.call(buf, ch => {
|
||||||
|
return String.fromCharCode(ch);
|
||||||
|
}).join('');
|
||||||
|
return btoa(binstr);
|
||||||
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
import { sqlEscaper } from 'common/libs/sqlEscaper';
|
import { sqlEscaper } from 'common/libs/sqlEscaper';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
export default class {
|
export default class {
|
||||||
static async getTableData (connection, schema, table) {
|
static async getTableData (connection, schema, table) {
|
||||||
return connection
|
return connection
|
||||||
@ -27,6 +29,13 @@ export default class {
|
|||||||
case 'longtext':
|
case 'longtext':
|
||||||
escapedParam = `"${sqlEscaper(params.content)}"`;
|
escapedParam = `"${sqlEscaper(params.content)}"`;
|
||||||
break;
|
break;
|
||||||
|
case 'blob':
|
||||||
|
case 'mediumblob':
|
||||||
|
case 'longblob': {
|
||||||
|
const fileBlob = fs.readFileSync(params.content);
|
||||||
|
escapedParam = `0x${fileBlob.toString('hex')}`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
escapedParam = `"${sqlEscaper(params.content)}"`;
|
escapedParam = `"${sqlEscaper(params.content)}"`;
|
||||||
break;
|
break;
|
||||||
|
@ -40,8 +40,7 @@ export default {
|
|||||||
ModalSettings: () => import(/* webpackChunkName: "ModalSettings" */'@/components/ModalSettings')
|
ModalSettings: () => import(/* webpackChunkName: "ModalSettings" */'@/components/ModalSettings')
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {};
|
||||||
};
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
@ -64,30 +63,30 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
html,
|
html,
|
||||||
body{
|
body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#wrapper{
|
#wrapper {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
#window-content{
|
#window-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
#main-content {
|
#main-content {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
height: calc(100vh - #{$excluding-size});
|
height: calc(100vh - #{$excluding-size});
|
||||||
width: calc(100% - #{$settingbar-width});
|
width: calc(100% - #{$settingbar-width});
|
||||||
|
|
||||||
> .columns{
|
> .columns {
|
||||||
height: calc(100vh - #{$footer-height});
|
height: calc(100vh - #{$footer-height});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -57,12 +57,53 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ConfirmModal>
|
</ConfirmModal>
|
||||||
|
<ConfirmModal
|
||||||
|
v-if="isBlobEditor"
|
||||||
|
@confirm="editOFF"
|
||||||
|
@hide="hideEditorModal"
|
||||||
|
>
|
||||||
|
<template :slot="'header'">
|
||||||
|
{{ $t('word.edit') }} "{{ field }}"
|
||||||
|
</template>
|
||||||
|
<div :slot="'body'">
|
||||||
|
<div class="mb-2">
|
||||||
|
<div>
|
||||||
|
<img
|
||||||
|
v-if="isImage"
|
||||||
|
:src="`data:${contentInfo.mime};base64, ${bufferToBase64(localContent)}`"
|
||||||
|
class="img-responsive p-centered"
|
||||||
|
>
|
||||||
|
<div v-if="contentInfo.size" class="editor-buttons mt-2">
|
||||||
|
<button class="btn btn-link btn-sm" @click="downloadFile">
|
||||||
|
<span>{{ $t('word.download') }}</span>
|
||||||
|
<i class="material-icons ml-1">file_download</i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="editor-field-info">
|
||||||
|
<div>
|
||||||
|
<b>{{ $t('word.size') }}</b>: {{ localContent.length | formatBytes }}<br>
|
||||||
|
<b>{{ $t('word.mimeType') }}</b>: {{ contentInfo.mime }}
|
||||||
|
</div>
|
||||||
|
<div><b>{{ $t('word.type') }}</b>: {{ type.toUpperCase() }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<label>{{ $t('message.uploadFile') }}</label>
|
||||||
|
<input
|
||||||
|
class="form-input"
|
||||||
|
type="file"
|
||||||
|
@change="filesChange($event)"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ConfirmModal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { mimeFromHex, formatBytes } from 'common/libs/utilities';
|
import { mimeFromHex, formatBytes, bufferToBase64 } from 'common/libs/utilities';
|
||||||
import hexToBinary from 'common/libs/hexToBinary';
|
import hexToBinary from 'common/libs/hexToBinary';
|
||||||
import { mask } from 'vue-the-mask';
|
import { mask } from 'vue-the-mask';
|
||||||
import ConfirmModal from '@/components/BaseConfirmModal';
|
import ConfirmModal from '@/components/BaseConfirmModal';
|
||||||
@ -73,6 +114,7 @@ export default {
|
|||||||
ConfirmModal
|
ConfirmModal
|
||||||
},
|
},
|
||||||
filters: {
|
filters: {
|
||||||
|
formatBytes,
|
||||||
cutText (val) {
|
cutText (val) {
|
||||||
if (typeof val !== 'string') return val;
|
if (typeof val !== 'string') return val;
|
||||||
return val.length > 128 ? `${val.substring(0, 128)}[...]` : val;
|
return val.length > 128 ? `${val.substring(0, 128)}[...]` : val;
|
||||||
@ -128,7 +170,14 @@ export default {
|
|||||||
return {
|
return {
|
||||||
isInlineEditor: false,
|
isInlineEditor: false,
|
||||||
isTextareaEditor: false,
|
isTextareaEditor: false,
|
||||||
localContent: null
|
isBlobEditor: false,
|
||||||
|
localContent: null,
|
||||||
|
contentInfo: {
|
||||||
|
ext: '',
|
||||||
|
mime: '',
|
||||||
|
size: null
|
||||||
|
},
|
||||||
|
fileToUpload: null
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -163,24 +212,52 @@ export default {
|
|||||||
default:
|
default:
|
||||||
return 'hidden';
|
return 'hidden';
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
isImage () {
|
||||||
|
return ['gif', 'jpg', 'png'].includes(this.contentInfo.ext);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
isNull (value) {
|
isNull (value) {
|
||||||
return value === null ? ' is-null' : '';
|
return value === null ? ' is-null' : '';
|
||||||
},
|
},
|
||||||
|
bufferToBase64 (val) {
|
||||||
|
return bufferToBase64(val);
|
||||||
|
},
|
||||||
editON () {
|
editON () {
|
||||||
if (['file'].includes(this.inputProps.type)) return;// TODO: remove temporary file block
|
|
||||||
this.localContent = this.$options.filters.typeFormat(this.content, this.type);
|
|
||||||
|
|
||||||
switch (this.type) {
|
switch (this.type) {
|
||||||
|
// Large text editor
|
||||||
case 'text':
|
case 'text':
|
||||||
case 'mediumtext':
|
case 'mediumtext':
|
||||||
case 'longtext':
|
case 'longtext':
|
||||||
this.isTextareaEditor = true;
|
this.isTextareaEditor = true;
|
||||||
|
this.localContent = this.$options.filters.typeFormat(this.content, this.type);
|
||||||
break;
|
break;
|
||||||
|
// File fields editor
|
||||||
|
case 'blob':
|
||||||
|
case 'mediumblob':
|
||||||
|
case 'longblob':
|
||||||
|
this.isBlobEditor = true;
|
||||||
|
this.localContent = this.content ? this.content : '';
|
||||||
|
this.fileToUpload = null;
|
||||||
|
|
||||||
default:// Inline editable fields
|
if (this.content !== null) {
|
||||||
|
const buff = Buffer.from(this.localContent);
|
||||||
|
if (buff.length) {
|
||||||
|
const hex = buff.toString('hex').substring(0, 8).toUpperCase();
|
||||||
|
const { ext, mime } = mimeFromHex(hex);
|
||||||
|
this.contentInfo = {
|
||||||
|
ext,
|
||||||
|
mime,
|
||||||
|
size: this.localContent.length
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
// Inline editable fields
|
||||||
|
default:
|
||||||
|
this.localContent = this.$options.filters.typeFormat(this.content, this.type);
|
||||||
this.$nextTick(() => { // Focus on input
|
this.$nextTick(() => { // Focus on input
|
||||||
this.$refs.cell.blur();
|
this.$refs.cell.blur();
|
||||||
|
|
||||||
@ -192,13 +269,41 @@ export default {
|
|||||||
},
|
},
|
||||||
editOFF () {
|
editOFF () {
|
||||||
this.isInlineEditor = false;
|
this.isInlineEditor = false;
|
||||||
if (this.localContent === this.$options.filters.typeFormat(this.content, this.type)) return;// If not changed
|
let content;
|
||||||
|
if (!['blob', 'mediumblob', 'longblob'].includes(this.type)) {
|
||||||
|
if (this.localContent === this.$options.filters.typeFormat(this.content, this.type)) return;// If not changed
|
||||||
|
content = this.localContent;
|
||||||
|
}
|
||||||
|
else { // Handle file upload
|
||||||
|
if (!this.fileToUpload) return;
|
||||||
|
content = this.fileToUpload.file.path;
|
||||||
|
}
|
||||||
|
|
||||||
const { field, type, localContent: content } = this;
|
this.$emit('updateField', {
|
||||||
this.$emit('updateField', { field, type, content });
|
field: this.field,
|
||||||
|
type: this.type,
|
||||||
|
content
|
||||||
|
});
|
||||||
},
|
},
|
||||||
hideEditorModal () {
|
hideEditorModal () {
|
||||||
this.isTextareaEditor = false;
|
this.isTextareaEditor = false;
|
||||||
|
this.isBlobEditor = false;
|
||||||
|
},
|
||||||
|
downloadFile () {
|
||||||
|
const downloadLink = document.createElement('a');
|
||||||
|
|
||||||
|
downloadLink.href = `data:${this.contentInfo.mime};base64, ${bufferToBase64(this.localContent)}`;
|
||||||
|
downloadLink.setAttribute('download', `${this.field}.${this.contentInfo.ext}`);
|
||||||
|
document.body.appendChild(downloadLink);
|
||||||
|
|
||||||
|
downloadLink.click();
|
||||||
|
downloadLink.remove();
|
||||||
|
},
|
||||||
|
filesChange (event) {
|
||||||
|
const { files } = event.target;
|
||||||
|
if (!files.length) return;
|
||||||
|
|
||||||
|
this.fileToUpload = { name: files[0].name, file: files[0] };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -228,5 +333,16 @@ export default {
|
|||||||
margin-top: 0.6rem;
|
margin-top: 0.6rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -32,7 +32,9 @@ module.exports = {
|
|||||||
results: 'Results',
|
results: 'Results',
|
||||||
size: 'Size',
|
size: 'Size',
|
||||||
seconds: 'Seconds',
|
seconds: 'Seconds',
|
||||||
type: 'Type'
|
type: 'Type',
|
||||||
|
mimeType: 'Mime-Type',
|
||||||
|
download: 'Download'
|
||||||
},
|
},
|
||||||
message: {
|
message: {
|
||||||
appWelcome: 'Welcome to Antares SQL Client!',
|
appWelcome: 'Welcome to Antares SQL Client!',
|
||||||
@ -59,7 +61,8 @@ module.exports = {
|
|||||||
editCell: 'Edit cell',
|
editCell: 'Edit cell',
|
||||||
deleteRows: 'Delete row | Delete {count} rows',
|
deleteRows: 'Delete row | Delete {count} rows',
|
||||||
confirmToDeleteRows: 'Do you confirm to delete one row? | Do you confirm to delete {count} rows?',
|
confirmToDeleteRows: 'Do you confirm to delete one row? | Do you confirm to delete {count} rows?',
|
||||||
notificationsTimeout: 'Notifications timeout'
|
notificationsTimeout: 'Notifications timeout',
|
||||||
|
uploadFile: 'Upload file'
|
||||||
},
|
},
|
||||||
// Date and Time
|
// Date and Time
|
||||||
short: {
|
short: {
|
||||||
|
Reference in New Issue
Block a user