PLT-3105 Files table migration (#4068)

* Implemented initial changes for files table

* Removed *_benchmark_test.go files

* Re-implemented GetPublicFile and added support for old path

* Localization for files table

* Moved file system code into utils package

* Finished server-side changes and added initial upgrade script

* Added getPostFiles api

* Re-add Extension and HasPreviewImage fields to FileInfo

* Removed unused translation

* Fixed merge conflicts left over after permissions changes

* Forced FileInfo.extension to be lower case

* Changed FileUploadResponse to contain the FileInfos instead of FileIds

* Fixed permissions on getFile* calls

* Fixed notifications for file uploads

* Added initial version of client code for files changes

* Permanently added FileIds field to Post object and removed Post.HasFiles

* Updated PostStore.Update to be usable in more circumstances

* Re-added Filenames field and switched file migration to be entirely lazy-loaded

* Increased max listener count for FileStore

* Removed unused fileInfoCache

* Moved file system code back into api

* Removed duplicate test case

* Fixed unit test running on ports other than 8065

* Renamed HasPermissionToPostContext to HasPermissionToChannelByPostContext

* Refactored handleImages to make it more easily understandable

* Renamed getPostFiles to getFileInfosForPost

* Re-added pre-FileIds posts to analytics

* Changed files to be saved as their ids as opposed to id/filename.ext

* Renamed FileInfo.UserId to FileInfo.CreatorId

* Fixed detection of language in CodePreview

* Fixed switching between threads in the RHS not loading new files

* Add serverside protection against a rare bug where the client sends the same file twice for a single post

* Refactored the important parts of uploadFile api call into a function that can be called without a web context
Этот коммит содержится в:
Harrison Healey
2016-09-30 11:06:30 -04:00
коммит произвёл GitHub
родитель a2deeed597
Коммит 8a0e649f98
60 изменённых файлов: 3454 добавлений и 2062 удалений

Просмотреть файл

@@ -1,204 +1,111 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import $ from 'jquery';
import ReactDOM from 'react-dom';
import * as utils from 'utils/utils.jsx';
import Client from 'client/web_client.jsx';
import Constants from 'utils/constants.jsx';
import FileStore from 'stores/file_store.jsx';
import * as Utils from 'utils/utils.jsx';
import {intlShape, injectIntl, defineMessages} from 'react-intl';
import {Tooltip, OverlayTrigger} from 'react-bootstrap';
const holders = defineMessages({
download: {
id: 'file_attachment.download',
defaultMessage: 'Download'
}
});
import React from 'react';
class FileAttachment extends React.Component {
export default class FileAttachment extends React.Component {
constructor(props) {
super(props);
this.loadFiles = this.loadFiles.bind(this);
this.addBackgroundImage = this.addBackgroundImage.bind(this);
this.onAttachmentClick = this.onAttachmentClick.bind(this);
this.canSetState = false;
this.state = {fileSize: -1};
this.state = {
loaded: Utils.getFileType(props.fileInfo.extension) !== 'image'
};
}
componentDidMount() {
this.loadFiles();
}
componentWillReceiveProps(nextProps) {
if (nextProps.fileInfo.id !== this.props.fileInfo.id) {
this.setState({
loaded: Utils.getFileType(nextProps.fileInfo.extension) !== 'image'
});
}
}
componentDidUpdate(prevProps) {
if (this.props.filename !== prevProps.filename) {
if (!this.state.loaded && this.props.fileInfo.id !== prevProps.fileInfo.id) {
this.loadFiles();
}
}
loadFiles() {
this.canSetState = true;
const fileInfo = this.props.fileInfo;
const fileType = Utils.getFileType(fileInfo.extension);
var filename = this.props.filename;
if (fileType === 'image') {
const thumbnailUrl = FileStore.getFileThumbnailUrl(fileInfo.id);
if (filename) {
var fileInfo = this.getFileInfoFromName(filename);
var type = utils.getFileType(fileInfo.ext);
if (type === 'image') {
var self = this; // Need this reference since we use the given "this"
$('<img/>').attr('src', fileInfo.path + '_thumb.jpg').on('load', (function loadWrapper(path, name) {
return function loader() {
$(this).remove();
if (name in self.refs) {
var imgDiv = ReactDOM.findDOMNode(self.refs[name]);
$(imgDiv).removeClass('post-image__load');
$(imgDiv).addClass('post-image');
var width = this.width || $(this).width();
var height = this.height || $(this).height();
if (width < Constants.THUMBNAIL_WIDTH &&
height < Constants.THUMBNAIL_HEIGHT) {
$(imgDiv).addClass('small');
} else {
$(imgDiv).addClass('normal');
}
self.addBackgroundImage(name, path);
}
};
}(fileInfo.path, filename)));
}
const img = new Image();
img.onload = () => {
this.setState({loaded: true});
};
img.load(thumbnailUrl);
}
}
componentWillUnmount() {
// keep track of when this component is mounted so that we can asynchronously change state without worrying about whether or not we're mounted
this.canSetState = false;
}
shouldComponentUpdate(nextProps, nextState) {
if (!utils.areObjectsEqual(nextProps, this.props)) {
return true;
}
// the only time this object should update is when it receives an updated file size which we can usually handle without re-rendering
if (nextState.fileSize !== this.state.fileSize) {
if (this.refs.fileSize) {
// update the UI element to display the file size without re-rendering the whole component
ReactDOM.findDOMNode(this.refs.fileSize).innerHTML = utils.fileSizeToString(nextState.fileSize);
return false;
}
// we can't find the element that should hold the file size so we must not have rendered yet
return true;
}
return true;
}
getFileInfoFromName(name) {
var fileInfo = utils.splitFileLocation(name);
fileInfo.path = Client.getFilesRoute() + '/get' + fileInfo.path;
return fileInfo;
}
addBackgroundImage(name, path) {
var fileUrl = path;
if (name in this.refs) {
if (!path) {
fileUrl = this.getFileInfoFromName(name).path;
}
var imgDiv = ReactDOM.findDOMNode(this.refs[name]);
var re1 = new RegExp(' ', 'g');
var re2 = new RegExp('\\(', 'g');
var re3 = new RegExp('\\)', 'g');
var url = fileUrl.replace(re1, '%20').replace(re2, '%28').replace(re3, '%29');
$(imgDiv).css('background-image', 'url(' + url + '_thumb.jpg)');
}
}
removeBackgroundImage(name) {
if (name in this.refs) {
$(ReactDOM.findDOMNode(this.refs[name])).css('background-image', 'initial');
}
}
onAttachmentClick(e) {
e.preventDefault();
this.props.handleImageClick(this.props.index);
}
render() {
var filename = this.props.filename;
const fileInfo = this.props.fileInfo;
const fileName = fileInfo.name;
const fileUrl = FileStore.getFileUrl(fileInfo.id);
var fileInfo = utils.splitFileLocation(filename);
var fileUrl = utils.getFileUrl(filename);
var type = utils.getFileType(fileInfo.ext);
let thumbnail;
if (this.state.loaded) {
const type = Utils.getFileType(fileInfo.extension);
var thumbnail;
if (type === 'image') {
thumbnail = (
<div
ref={filename}
className='post-image__load'
/>
);
} else {
thumbnail = <div className={'file-icon ' + utils.getIconClassName(type)}/>;
}
if (type === 'image') {
let className = 'post-image';
var fileSizeString = '';
if (this.state.fileSize < 0) {
Client.getFileInfo(
filename,
(data) => {
if (this.canSetState) {
this.setState({fileSize: parseInt(data.size, 10)});
}
},
() => {
// Do nothing
if (fileInfo.width < Constants.THUMBNAIL_WIDTH && fileInfo.height < Constants.THUMBNAIL_HEIGHT) {
className += ' small';
} else {
className += ' normal';
}
);
thumbnail = (
<div
className={className}
style={{
backgroundImage: `url(${FileStore.getFileThumbnailUrl(fileInfo.id)})`
}}
/>
);
} else {
thumbnail = <div className={'file-icon ' + Utils.getIconClassName(type)}/>;
}
} else {
fileSizeString = utils.fileSizeToString(this.state.fileSize);
thumbnail = <div className='post-image__load'/>;
}
var filenameString = decodeURIComponent(utils.getFileName(filename));
var trimmedFilename;
if (filenameString.length > 35) {
trimmedFilename = filenameString.substring(0, Math.min(35, filenameString.length)) + '...';
let trimmedFilename;
if (fileName.length > 35) {
trimmedFilename = fileName.substring(0, Math.min(35, fileName.length)) + '...';
} else {
trimmedFilename = filenameString;
trimmedFilename = fileName;
}
var filenameOverlay = (
<OverlayTrigger
delayShow={1000}
placement='top'
overlay={<Tooltip id='file-name__tooltip'>{this.props.intl.formatMessage(holders.download) + ' "' + filenameString + '"'}</Tooltip>}
>
<a
href={fileUrl}
download={filenameString}
className='post-image__name'
target='_blank'
rel='noopener noreferrer'
>
{trimmedFilename}
</a>
</OverlayTrigger>
);
let filenameOverlay;
if (this.props.compactDisplay) {
filenameOverlay = (
<OverlayTrigger
delayShow={1000}
placement='top'
overlay={<Tooltip id='file-name__tooltip'>{filenameString}</Tooltip>}
overlay={<Tooltip id='file-name__tooltip'>{fileName}</Tooltip>}
>
<a
href='#'
@@ -214,13 +121,28 @@ class FileAttachment extends React.Component {
</a>
</OverlayTrigger>
);
} else {
filenameOverlay = (
<OverlayTrigger
delayShow={1000}
placement='top'
overlay={<Tooltip id='file-name__tooltip'>{Utils.localizeMessage('file_attachment.download', 'Download') + ' "' + fileName + '"'}</Tooltip>}
>
<a
href={fileUrl}
download={fileName}
className='post-image__name'
target='_blank'
rel='noopener noreferrer'
>
{trimmedFilename}
</a>
</OverlayTrigger>
);
}
return (
<div
className='post-image__column'
key={filename}
>
<div className='post-image__column'>
<a
className='post-image__thumbnail'
href='#'
@@ -233,17 +155,15 @@ class FileAttachment extends React.Component {
<div>
<a
href={fileUrl}
download={filenameString}
download={fileName}
className='post-image__download'
target='_blank'
rel='noopener noreferrer'
>
<span
className='fa fa-download'
/>
<span className='fa fa-download'/>
</a>
<span className='post-image__type'>{fileInfo.ext.toUpperCase()}</span>
<span className='post-image__size'>{fileSizeString}</span>
<span className='post-image__type'>{fileInfo.extension.toUpperCase()}</span>
<span className='post-image__size'>{Utils.fileSizeToString(fileInfo.size)}</span>
</div>
</div>
</div>
@@ -252,10 +172,7 @@ class FileAttachment extends React.Component {
}
FileAttachment.propTypes = {
intl: intlShape.isRequired,
// a list of file pathes displayed by the parent FileAttachmentList
filename: React.PropTypes.string.isRequired,
fileInfo: React.PropTypes.object.isRequired,
// the index of this attachment preview in the parent FileAttachmentList
index: React.PropTypes.number.isRequired,
@@ -264,6 +181,4 @@ FileAttachment.propTypes = {
handleImageClick: React.PropTypes.func,
compactDisplay: React.PropTypes.bool
};
export default injectIntl(FileAttachment);
};