Commit f1bb4977 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'himkp-webide-binary-regression' into 'master'

Fix regression when uploading / viewing binary files in the Web IDE

See merge request gitlab-org/gitlab!44699
parents 14e05a8b 862ad57c
...@@ -35,7 +35,7 @@ export default { ...@@ -35,7 +35,7 @@ export default {
name: `${this.path ? `${this.path}/` : ''}${name}`, name: `${this.path ? `${this.path}/` : ''}${name}`,
type: 'blob', type: 'blob',
content, content,
rawPath: !isText ? target.result : '', rawPath: !isText ? URL.createObjectURL(file) : '',
}); });
if (isText) { if (isText) {
...@@ -44,7 +44,7 @@ export default { ...@@ -44,7 +44,7 @@ export default {
reader.addEventListener('load', e => emitCreateEvent(e.target.result), { once: true }); reader.addEventListener('load', e => emitCreateEvent(e.target.result), { once: true });
reader.readAsText(file); reader.readAsText(file);
} else { } else {
emitCreateEvent(encodedContent); emitCreateEvent(rawContent);
} }
}, },
readFile(file) { readFile(file) {
......
...@@ -68,7 +68,7 @@ export default { ...@@ -68,7 +68,7 @@ export default {
]), ]),
...mapGetters('fileTemplates', ['showFileTemplatesBar']), ...mapGetters('fileTemplates', ['showFileTemplatesBar']),
shouldHideEditor() { shouldHideEditor() {
return this.file && !isTextFile(this.file); return this.file && !this.file.loading && !isTextFile(this.file);
}, },
showContentViewer() { showContentViewer() {
return ( return (
...@@ -235,6 +235,7 @@ export default { ...@@ -235,6 +235,7 @@ export default {
return this.getFileData({ return this.getFileData({
path: this.file.path, path: this.file.path,
makeFileActive: false, makeFileActive: false,
toggleLoading: false,
}).then(() => }).then(() =>
this.getRawFileData({ this.getRawFileData({
path: this.file.path, path: this.file.path,
......
...@@ -157,8 +157,10 @@ export default class Editor { ...@@ -157,8 +157,10 @@ export default class Editor {
} }
updateDimensions() { updateDimensions() {
this.instance.layout(); if (this.instance) {
this.updateDiffView(); this.instance.layout();
this.updateDiffView();
}
} }
setPosition({ lineNumber, column }) { setPosition({ lineNumber, column }) {
......
...@@ -59,7 +59,7 @@ export const setFileActive = ({ commit, state, getters, dispatch }, path) => { ...@@ -59,7 +59,7 @@ export const setFileActive = ({ commit, state, getters, dispatch }, path) => {
export const getFileData = ( export const getFileData = (
{ state, commit, dispatch, getters }, { state, commit, dispatch, getters },
{ path, makeFileActive = true, openFile = makeFileActive }, { path, makeFileActive = true, openFile = makeFileActive, toggleLoading = true },
) => { ) => {
const file = state.entries[path]; const file = state.entries[path];
const fileDeletedAndReadded = getters.isFileDeletedAndReadded(path); const fileDeletedAndReadded = getters.isFileDeletedAndReadded(path);
...@@ -99,7 +99,7 @@ export const getFileData = ( ...@@ -99,7 +99,7 @@ export const getFileData = (
}); });
}) })
.finally(() => { .finally(() => {
commit(types.TOGGLE_LOADING, { entry: file, forceValue: false }); if (toggleLoading) commit(types.TOGGLE_LOADING, { entry: file, forceValue: false });
}); });
}; };
......
...@@ -19,19 +19,20 @@ export default { ...@@ -19,19 +19,20 @@ export default {
} }
}, },
[types.TOGGLE_FILE_OPEN](state, path) { [types.TOGGLE_FILE_OPEN](state, path) {
Object.assign(state.entries[path], { const entry = state.entries[path];
opened: !state.entries[path].opened,
});
if (state.entries[path].opened) { entry.opened = !entry.opened;
if (entry.opened && !entry.tempFile) {
entry.loading = true;
}
if (entry.opened) {
Object.assign(state, { Object.assign(state, {
openFiles: state.openFiles.filter(f => f.path !== path).concat(state.entries[path]), openFiles: state.openFiles.filter(f => f.path !== path).concat(state.entries[path]),
}); });
} else { } else {
const file = state.entries[path];
Object.assign(state, { Object.assign(state, {
openFiles: state.openFiles.filter(f => f.key !== file.key), openFiles: state.openFiles.filter(f => f.key !== entry.key),
}); });
} }
}, },
......
...@@ -3,7 +3,7 @@ import { ...@@ -3,7 +3,7 @@ import {
relativePathToAbsolute, relativePathToAbsolute,
isAbsolute, isAbsolute,
isRootRelative, isRootRelative,
isBase64DataUrl, isBlobUrl,
} from '~/lib/utils/url_utility'; } from '~/lib/utils/url_utility';
export const dataStructure = () => ({ export const dataStructure = () => ({
...@@ -110,14 +110,19 @@ export const createCommitPayload = ({ ...@@ -110,14 +110,19 @@ export const createCommitPayload = ({
}) => ({ }) => ({
branch, branch,
commit_message: state.commitMessage || getters.preBuiltCommitMessage, commit_message: state.commitMessage || getters.preBuiltCommitMessage,
actions: getCommitFiles(rootState.stagedFiles).map(f => ({ actions: getCommitFiles(rootState.stagedFiles).map(f => {
action: commitActionForFile(f), const isBlob = isBlobUrl(f.rawPath);
file_path: f.path, const content = isBlob ? btoa(f.content) : f.content;
previous_path: f.prevPath || undefined,
content: f.prevPath && !f.changed ? null : f.content || undefined, return {
encoding: isBase64DataUrl(f.rawPath) ? 'base64' : 'text', action: commitActionForFile(f),
last_commit_id: newBranch || f.deleted || f.prevPath ? undefined : f.lastCommitSha, file_path: f.path,
})), previous_path: f.prevPath || undefined,
content: f.prevPath && !f.changed ? null : content || undefined,
encoding: isBlob ? 'base64' : 'text',
last_commit_id: newBranch || f.deleted || f.prevPath ? undefined : f.lastCommitSha,
};
}),
start_sha: newBranch ? rootGetters.lastCommit.id : undefined, start_sha: newBranch ? rootGetters.lastCommit.id : undefined,
}); });
......
...@@ -43,16 +43,17 @@ const KNOWN_TYPES = [ ...@@ -43,16 +43,17 @@ const KNOWN_TYPES = [
}, },
]; ];
export function isTextFile({ name, content, mimeType = '' }) { export function isTextFile({ name, raw, content, mimeType = '' }) {
const knownType = KNOWN_TYPES.find(type => type.isMatch(mimeType, name)); const knownType = KNOWN_TYPES.find(type => type.isMatch(mimeType, name));
if (knownType) return knownType.isText; if (knownType) return knownType.isText;
// does the string contain ascii characters only (ranges from space to tilde, tabs and new lines) // does the string contain ascii characters only (ranges from space to tilde, tabs and new lines)
const asciiRegex = /^[ -~\t\n\r]+$/; const asciiRegex = /^[ -~\t\n\r]+$/;
const fileContents = raw || content;
// for unknown types, determine the type by evaluating the file contents // for unknown types, determine the type by evaluating the file contents
return isString(content) && (content === '' || asciiRegex.test(content)); return isString(fileContents) && (fileContents === '' || asciiRegex.test(fileContents));
} }
export const createPathWithExt = p => { export const createPathWithExt = p => {
......
...@@ -291,6 +291,15 @@ export function isBase64DataUrl(url) { ...@@ -291,6 +291,15 @@ export function isBase64DataUrl(url) {
return /^data:[.\w+-]+\/[.\w+-]+;base64,/.test(url); return /^data:[.\w+-]+\/[.\w+-]+;base64,/.test(url);
} }
/**
* Returns true if url is a blob: type url
*
* @param {String} url
*/
export function isBlobUrl(url) {
return /^blob:/.test(url);
}
/** /**
* Returns true if url is an absolute or root-relative URL * Returns true if url is an absolute or root-relative URL
* *
......
...@@ -4,6 +4,11 @@ import ImageViewer from './viewers/image_viewer.vue'; ...@@ -4,6 +4,11 @@ import ImageViewer from './viewers/image_viewer.vue';
import DownloadViewer from './viewers/download_viewer.vue'; import DownloadViewer from './viewers/download_viewer.vue';
export default { export default {
components: {
MarkdownViewer,
ImageViewer,
DownloadViewer,
},
props: { props: {
content: { content: {
type: String, type: String,
...@@ -45,35 +50,25 @@ export default { ...@@ -45,35 +50,25 @@ export default {
default: () => ({}), default: () => ({}),
}, },
}, },
computed: {
viewer() {
if (!this.path) return null;
if (!this.type) return DownloadViewer;
switch (this.type) {
case 'markdown':
return MarkdownViewer;
case 'image':
return ImageViewer;
default:
return DownloadViewer;
}
},
},
}; };
</script> </script>
<template> <template>
<div class="preview-container"> <div class="preview-container">
<component <image-viewer v-if="type === 'image'" :path="path" :file-size="fileSize" />
:is="viewer" <markdown-viewer
:path="path" v-if="type === 'markdown'"
:content="content"
:commit-sha="commitSha"
:file-path="filePath" :file-path="filePath"
:file-size="fileSize"
:project-path="projectPath" :project-path="projectPath"
:content="content"
:images="images" :images="images"
:commit-sha="commitSha" />
<download-viewer
v-if="!type && path"
:path="path"
:file-path="filePath"
:file-size="fileSize"
/> />
</div> </div>
</template> </template>
<script> <script>
import { GlLink, GlIcon } from '@gitlab/ui'; import { GlIcon } from '@gitlab/ui';
import { numberToHumanSize } from '../../../../lib/utils/number_utils'; import { numberToHumanSize } from '../../../../lib/utils/number_utils';
export default { export default {
components: { components: {
GlLink,
GlIcon, GlIcon,
}, },
props: { props: {
...@@ -44,16 +43,10 @@ export default { ...@@ -44,16 +43,10 @@ export default {
({{ fileSizeReadable }}) ({{ fileSizeReadable }})
</template> </template>
</p> </p>
<gl-link <a :href="path" class="btn btn-default" rel="nofollow" :download="fileName" target="_blank">
:href="path"
class="btn btn-default"
rel="nofollow"
:download="fileName"
target="_blank"
>
<gl-icon :size="16" name="download" class="float-left gl-mr-3" /> <gl-icon :size="16" name="download" class="float-left gl-mr-3" />
{{ __('Download') }} {{ __('Download') }}
</gl-link> </a>
</div> </div>
</div> </div>
</template> </template>
---
title: Fix regression when uploading / viewing binary files in the Web IDE
merge_request: 44699
author:
type: fixed
...@@ -8,13 +8,6 @@ describe('Sketch viewer', () => { ...@@ -8,13 +8,6 @@ describe('Sketch viewer', () => {
beforeEach(() => { beforeEach(() => {
loadFixtures('static/sketch_viewer.html'); loadFixtures('static/sketch_viewer.html');
window.URL = {
createObjectURL: jest.fn(() => 'http://foo/bar'),
};
});
afterEach(() => {
window.URL = {};
}); });
describe('with error message', () => { describe('with error message', () => {
......
URL.createObjectURL = function createObjectURL() {
return 'blob:https://gitlab.com/048c7ac1-98de-4a37-ab1b-0206d0ea7e1b';
};
import './create_object_url';
import './element_scroll_into_view'; import './element_scroll_into_view';
import './element_scroll_by'; import './element_scroll_by';
import './element_scroll_to'; import './element_scroll_to';
......
...@@ -59,14 +59,11 @@ describe('new dropdown upload', () => { ...@@ -59,14 +59,11 @@ describe('new dropdown upload', () => {
result: 'base64,cGxhaW4gdGV4dA==', result: 'base64,cGxhaW4gdGV4dA==',
}; };
const binaryTarget = { const binaryTarget = {
result: 'base64,w4I=', result: 'base64,8PDw8A==', // ðððð
}; };
const textFile = new File(['plain text'], 'textFile');
const binaryFile = { const textFile = new File(['plain text'], 'textFile');
name: 'binaryFile', const binaryFile = new File(['😺'], 'binaryFile');
type: 'image/png',
};
beforeEach(() => { beforeEach(() => {
jest.spyOn(FileReader.prototype, 'readAsText'); jest.spyOn(FileReader.prototype, 'readAsText');
...@@ -92,16 +89,16 @@ describe('new dropdown upload', () => { ...@@ -92,16 +89,16 @@ describe('new dropdown upload', () => {
.catch(done.fail); .catch(done.fail);
}); });
it('splits content on base64 if binary', () => { it('creates a blob URL for the content if binary', () => {
vm.createFile(binaryTarget, binaryFile); vm.createFile(binaryTarget, binaryFile);
expect(FileReader.prototype.readAsText).not.toHaveBeenCalledWith(textFile); expect(FileReader.prototype.readAsText).not.toHaveBeenCalled();
expect(vm.$emit).toHaveBeenCalledWith('create', { expect(vm.$emit).toHaveBeenCalledWith('create', {
name: binaryFile.name, name: binaryFile.name,
type: 'blob', type: 'blob',
content: binaryTarget.result.split('base64,')[1], content: 'ðððð',
rawPath: binaryTarget.result, rawPath: 'blob:https://gitlab.com/048c7ac1-98de-4a37-ab1b-0206d0ea7e1b',
}); });
}); });
}); });
......
...@@ -291,6 +291,20 @@ describe('IDE store file actions', () => { ...@@ -291,6 +291,20 @@ describe('IDE store file actions', () => {
expect(store.state.openFiles[0].name).toBe(localFile.name); expect(store.state.openFiles[0].name).toBe(localFile.name);
}); });
}); });
it('does not toggle loading if toggleLoading=false', () => {
expect(localFile.loading).toBe(false);
return store
.dispatch('getFileData', {
path: localFile.path,
makeFileActive: false,
toggleLoading: false,
})
.then(() => {
expect(localFile.loading).toBe(true);
});
});
}); });
describe('Re-named success', () => { describe('Re-named success', () => {
......
...@@ -39,20 +39,34 @@ describe('IDE store file mutations', () => { ...@@ -39,20 +39,34 @@ describe('IDE store file mutations', () => {
}); });
describe('TOGGLE_FILE_OPEN', () => { describe('TOGGLE_FILE_OPEN', () => {
beforeEach(() => { it('adds into opened files', () => {
mutations.TOGGLE_FILE_OPEN(localState, localFile.path); mutations.TOGGLE_FILE_OPEN(localState, localFile.path);
});
it('adds into opened files', () => {
expect(localFile.opened).toBeTruthy(); expect(localFile.opened).toBeTruthy();
expect(localState.openFiles.length).toBe(1); expect(localState.openFiles.length).toBe(1);
}); });
it('removes from opened files', () => { describe('if already open', () => {
it('removes from opened files', () => {
mutations.TOGGLE_FILE_OPEN(localState, localFile.path);
mutations.TOGGLE_FILE_OPEN(localState, localFile.path);
expect(localFile.opened).toBeFalsy();
expect(localState.openFiles.length).toBe(0);
});
});
it.each`
entry | loading
${{ opened: false }} | ${true}
${{ opened: false, tempFile: true }} | ${false}
${{ opened: true }} | ${false}
`('for state: $entry, sets loading=$loading', ({ entry, loading }) => {
Object.assign(localFile, entry);
mutations.TOGGLE_FILE_OPEN(localState, localFile.path); mutations.TOGGLE_FILE_OPEN(localState, localFile.path);
expect(localFile.opened).toBeFalsy(); expect(localFile.loading).toBe(loading);
expect(localState.openFiles.length).toBe(0);
}); });
}); });
......
...@@ -46,7 +46,7 @@ describe('Multi-file store utils', () => { ...@@ -46,7 +46,7 @@ describe('Multi-file store utils', () => {
path: 'added', path: 'added',
tempFile: true, tempFile: true,
content: 'new file content', content: 'new file content',
rawPath: 'data:image/png;base64,abc', rawPath: 'blob:https://gitlab.com/048c7ac1-98de-4a37-ab1b-0206d0ea7e1b',
lastCommitSha: '123456789', lastCommitSha: '123456789',
}, },
{ ...file('deletedFile'), path: 'deletedFile', deleted: true }, { ...file('deletedFile'), path: 'deletedFile', deleted: true },
...@@ -77,7 +77,8 @@ describe('Multi-file store utils', () => { ...@@ -77,7 +77,8 @@ describe('Multi-file store utils', () => {
{ {
action: commitActionTypes.create, action: commitActionTypes.create,
file_path: 'added', file_path: 'added',
content: 'new file content', // atob("new file content")
content: 'bmV3IGZpbGUgY29udGVudA==',
encoding: 'base64', encoding: 'base64',
last_commit_id: '123456789', last_commit_id: '123456789',
previous_path: undefined, previous_path: undefined,
...@@ -117,7 +118,7 @@ describe('Multi-file store utils', () => { ...@@ -117,7 +118,7 @@ describe('Multi-file store utils', () => {
path: 'added', path: 'added',
tempFile: true, tempFile: true,
content: 'new file content', content: 'new file content',
rawPath: 'data:image/png;base64,abc', rawPath: 'blob:https://gitlab.com/048c7ac1-98de-4a37-ab1b-0206d0ea7e1b',
lastCommitSha: '123456789', lastCommitSha: '123456789',
}, },
], ],
...@@ -148,7 +149,8 @@ describe('Multi-file store utils', () => { ...@@ -148,7 +149,8 @@ describe('Multi-file store utils', () => {
{ {
action: commitActionTypes.create, action: commitActionTypes.create,
file_path: 'added', file_path: 'added',
content: 'new file content', // atob("new file content")
content: 'bmV3IGZpbGUgY29udGVudA==',
encoding: 'base64', encoding: 'base64',
last_commit_id: '123456789', last_commit_id: '123456789',
previous_path: undefined, previous_path: undefined,
......
...@@ -509,6 +509,20 @@ describe('URL utility', () => { ...@@ -509,6 +509,20 @@ describe('URL utility', () => {
}); });
}); });
describe('isBlobUrl', () => {
it.each`
url | valid
${undefined} | ${false}
${'blob:http://gitlab.com/abcd'} | ${true}
${'data:image/png;base64,abcdef'} | ${false}
${'notaurl'} | ${false}
${'../relative_url'} | ${false}
${'<a></a>'} | ${false}
`('returns $valid for $url', ({ url, valid }) => {
expect(urlUtils.isBlobUrl(url)).toBe(valid);
});
});
describe('relativePathToAbsolute', () => { describe('relativePathToAbsolute', () => {
it.each` it.each`
path | base | result path | base | result
......
...@@ -38,8 +38,6 @@ import MonitorStackedColumnChart from '~/monitoring/components/charts/stacked_co ...@@ -38,8 +38,6 @@ import MonitorStackedColumnChart from '~/monitoring/components/charts/stacked_co
import { createStore, monitoringDashboard } from '~/monitoring/stores'; import { createStore, monitoringDashboard } from '~/monitoring/stores';
import { createStore as createEmbedGroupStore } from '~/monitoring/stores/embed_group'; import { createStore as createEmbedGroupStore } from '~/monitoring/stores/embed_group';
global.URL.createObjectURL = jest.fn();
const mocks = { const mocks = {
$toast: { $toast: {
show: jest.fn(), show: jest.fn(),
...@@ -94,6 +92,8 @@ describe('Dashboard Panel', () => { ...@@ -94,6 +92,8 @@ describe('Dashboard Panel', () => {
state = store.state.monitoringDashboard; state = store.state.monitoringDashboard;
axiosMock = new AxiosMockAdapter(axios); axiosMock = new AxiosMockAdapter(axios);
jest.spyOn(URL, 'createObjectURL');
}); });
afterEach(() => { afterEach(() => {
......
...@@ -9,7 +9,6 @@ describe('performance bar wrapper', () => { ...@@ -9,7 +9,6 @@ describe('performance bar wrapper', () => {
let vm; let vm;
beforeEach(() => { beforeEach(() => {
URL.createObjectURL = jest.fn();
performance.getEntriesByType = jest.fn().mockReturnValue([]); performance.getEntriesByType = jest.fn().mockReturnValue([]);
// clear html so that elements from previous tests don't mess with this test // clear html so that elements from previous tests don't mess with this test
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment