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
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
a2deeed597
Коммит
8a0e649f98
@@ -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);
|
||||
};
|
||||
Ссылка в новой задаче
Block a user