Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
301 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
from __future__ import absolute_import | ||
|
||
import posixpath | ||
import six | ||
|
||
try: | ||
from django.http import (CompatibleStreamingHttpResponse as StreamingHttpResponse) | ||
except ImportError: | ||
from django.http import StreamingHttpResponse | ||
|
||
from sentry import features | ||
from sentry.api.bases.project import ProjectEndpoint | ||
from sentry.models import Event, EventAttachment | ||
|
||
|
||
class EventAttachmentDetailsEndpoint(ProjectEndpoint): | ||
def download(self, attachment): | ||
file = attachment.file | ||
fp = file.getfile() | ||
response = StreamingHttpResponse( | ||
iter(lambda: fp.read(4096), b''), | ||
content_type=file.headers.get('content-type', 'application/octet-stream'), | ||
) | ||
response['Content-Length'] = file.size | ||
response['Content-Disposition'] = 'attachment; filename="%s"' % posixpath.basename( | ||
" ".join(attachment.name.split()) | ||
) | ||
return response | ||
|
||
def get(self, request, project, event_id, attachment_id): | ||
""" | ||
Retrieve an Attachment | ||
`````````````````````` | ||
:pparam string organization_slug: the slug of the organization the | ||
issues belong to. | ||
:pparam string project_slug: the slug of the project the event | ||
belongs to. | ||
:pparam string event_id: the id of the event. | ||
:pparam string attachment_id: the id of the attachment. | ||
:auth: required | ||
""" | ||
if not features.has('organizations:event-attachments', project.organization, actor=request.user): | ||
return self.respond(status=404) | ||
|
||
try: | ||
event = Event.objects.get( | ||
id=event_id, | ||
project_id=project.id, | ||
) | ||
except Event.DoesNotExist: | ||
return self.respond({'detail': 'Event not found'}, status=404) | ||
|
||
try: | ||
attachment = EventAttachment.objects.filter( | ||
project_id=project.id, | ||
event_id=event.event_id, | ||
id=attachment_id, | ||
).select_related('file').get() | ||
except EventAttachment.DoesNotExist: | ||
return self.respond({'detail': 'Attachment not found'}, status=404) | ||
|
||
if request.GET.get('download') is not None: | ||
return self.download(attachment) | ||
|
||
return self.respond({ | ||
'id': six.text_type(attachment.id), | ||
'name': attachment.name, | ||
'headers': attachment.file.headers, | ||
'size': attachment.file.size, | ||
'sha1': attachment.file.checksum, | ||
'dateCreated': attachment.file.timestamp, | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
109 changes: 109 additions & 0 deletions
109
src/sentry/static/sentry/app/components/events/eventAttachments.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
import PropTypes from 'prop-types'; | ||
import React from 'react'; | ||
import createReactClass from 'create-react-class'; | ||
import {Box} from 'grid-emotion'; | ||
|
||
import ApiMixin from 'app/mixins/apiMixin'; | ||
import FileSize from 'app/components/fileSize'; | ||
import GroupState from 'app/mixins/groupState'; | ||
|
||
import {t} from 'app/locale'; | ||
|
||
import {Panel, PanelBody, PanelItem} from 'app/components/panels'; | ||
|
||
export default createReactClass({ | ||
displayName: 'EventAttachments', | ||
|
||
propTypes: { | ||
event: PropTypes.object.isRequired, | ||
orgId: PropTypes.string.isRequired, | ||
projectId: PropTypes.string.isRequired, | ||
}, | ||
|
||
mixins: [ApiMixin, GroupState], | ||
|
||
getInitialState() { | ||
return {attachmentList: undefined, expanded: false}; | ||
}, | ||
|
||
componentDidMount() { | ||
this.fetchData(this.props.event); | ||
}, | ||
|
||
componentWillReceiveProps(nextProps) { | ||
if (this.props.event && nextProps.event) { | ||
if (this.props.event.id !== nextProps.event.id) { | ||
//two events, with different IDs | ||
this.fetchData(nextProps.event); | ||
} | ||
} else if (nextProps.event) { | ||
//going from having no event to having an event | ||
this.fetchData(nextProps.event); | ||
} | ||
}, | ||
|
||
fetchData(event) { | ||
// TODO(dcramer): this API request happens twice, and we need a store for it | ||
if (!event) return; | ||
this.api.request( | ||
`/projects/${this.props.orgId}/${this.props | ||
.projectId}/events/${event.id}/attachments/`, | ||
{ | ||
success: (data, _, jqXHR) => { | ||
this.setState({ | ||
attachmentList: data, | ||
}); | ||
}, | ||
error: error => { | ||
this.setState({ | ||
attachmentList: undefined, | ||
}); | ||
}, | ||
} | ||
); | ||
}, | ||
|
||
getDownloadUrl(attachment) { | ||
let {orgId, event, projectId} = this.props; | ||
return `/api/0/projects/${orgId}/${projectId}/events/${event.id}/attachments/${attachment.id}/?download=1`; | ||
}, | ||
|
||
render() { | ||
let {attachmentList} = this.state; | ||
if (!(attachmentList && attachmentList.length)) { | ||
return null; | ||
} | ||
|
||
return ( | ||
<div className="box"> | ||
<div className="box-header"> | ||
<h3> | ||
{t('Attachments')} ({attachmentList.length}) | ||
</h3> | ||
<Panel> | ||
<PanelBody> | ||
{attachmentList.map(attachment => { | ||
return ( | ||
<PanelItem key={attachment.id} align="center"> | ||
<Box | ||
flex={10} | ||
pr={1} | ||
style={{wordWrap: 'break-word', wordBreak: 'break-all'}} | ||
> | ||
<a href={this.getDownloadUrl(attachment)}> | ||
<strong>{attachment.name}</strong> | ||
</a> | ||
</Box> | ||
<Box flex={1} textAlign="right"> | ||
<FileSize bytes={attachment.size} /> | ||
</Box> | ||
</PanelItem> | ||
); | ||
})} | ||
</PanelBody> | ||
</Panel> | ||
</div> | ||
</div> | ||
); | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
90 changes: 90 additions & 0 deletions
90
tests/sentry/api/endpoints/test_event_attachment_details.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
from __future__ import absolute_import | ||
|
||
import six | ||
|
||
from datetime import datetime | ||
from six import BytesIO | ||
|
||
from sentry.models import EventAttachment, File | ||
from sentry.testutils import APITestCase | ||
|
||
|
||
class EventAttachmentDetailsTest(APITestCase): | ||
def test_simple(self): | ||
self.login_as(user=self.user) | ||
|
||
project = self.create_project() | ||
release = self.create_release(project, self.user) | ||
group = self.create_group(project=project, first_release=release) | ||
event1 = self.create_event( | ||
event_id='a', | ||
group=group, | ||
datetime=datetime(2016, 8, 13, 3, 8, 25), | ||
tags={'sentry:release': release.version} | ||
) | ||
attachment1 = EventAttachment.objects.create( | ||
event_id=event1.event_id, | ||
group_id=event1.group_id, | ||
project_id=event1.project_id, | ||
file=File.objects.create( | ||
name='hello.png', | ||
type='image/png', | ||
), | ||
name='hello.png', | ||
) | ||
|
||
path = '/api/0/projects/{}/{}/events/{}/attachments/{}/'.format( | ||
event1.project.organization.slug, | ||
event1.project.slug, | ||
event1.id, | ||
attachment1.id, | ||
) | ||
|
||
with self.feature('organizations:event-attachments'): | ||
response = self.client.get(path) | ||
|
||
assert response.status_code == 200, response.content | ||
assert response.data['id'] == six.text_type(attachment1.id) | ||
|
||
def test_download(self): | ||
self.login_as(user=self.user) | ||
|
||
project = self.create_project() | ||
release = self.create_release(project, self.user) | ||
group = self.create_group(project=project, first_release=release) | ||
event1 = self.create_event( | ||
event_id='a', | ||
group=group, | ||
datetime=datetime(2016, 8, 13, 3, 8, 25), | ||
tags={'sentry:release': release.version} | ||
) | ||
|
||
file1 = File.objects.create( | ||
name='hello.png', | ||
type='image/png', | ||
) | ||
file1.putfile(BytesIO('File contents here')) | ||
|
||
attachment1 = EventAttachment.objects.create( | ||
event_id=event1.event_id, | ||
group_id=event1.group_id, | ||
project_id=event1.project_id, | ||
file=file1, | ||
name='hello.png', | ||
) | ||
|
||
path = '/api/0/projects/{}/{}/events/{}/attachments/{}/?download'.format( | ||
event1.project.organization.slug, | ||
event1.project.slug, | ||
event1.id, | ||
attachment1.id, | ||
) | ||
|
||
with self.feature('organizations:event-attachments'): | ||
response = self.client.get(path) | ||
|
||
assert response.status_code == 200, response.content | ||
assert response.get('Content-Disposition') == 'attachment; filename="hello.png"' | ||
assert response.get('Content-Length') == six.text_type(file1.size) | ||
assert response.get('Content-Type') == 'application/octet-stream' | ||
assert 'File contents here' == BytesIO(b"".join(response.streaming_content)).getvalue() |