1
Fork 0

Add download support

This commit is contained in:
viktorstrate 2020-02-21 22:42:39 +01:00
parent 37aea1fb3a
commit e00a5553f7
13 changed files with 313 additions and 72 deletions

View File

@ -106,8 +106,10 @@ type ComplexityRoot struct {
}
PhotoDownload struct {
Title func(childComplexity int) int
URL func(childComplexity int) int
Height func(childComplexity int) int
Title func(childComplexity int) int
URL func(childComplexity int) int
Width func(childComplexity int) int
}
PhotoExif struct {
@ -571,6 +573,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Photo.Title(childComplexity), true
case "PhotoDownload.height":
if e.complexity.PhotoDownload.Height == nil {
break
}
return e.complexity.PhotoDownload.Height(childComplexity), true
case "PhotoDownload.title":
if e.complexity.PhotoDownload.Title == nil {
break
@ -585,6 +594,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.PhotoDownload.URL(childComplexity), true
case "PhotoDownload.width":
if e.complexity.PhotoDownload.Width == nil {
break
}
return e.complexity.PhotoDownload.Width(childComplexity), true
case "PhotoEXIF.aperture":
if e.complexity.PhotoExif.Aperture == nil {
break
@ -1151,6 +1167,8 @@ type PhotoURL {
type PhotoDownload {
title: String!
width: Int!
height: Int!
url: String!
}
@ -3252,6 +3270,80 @@ func (ec *executionContext) _PhotoDownload_title(ctx context.Context, field grap
return ec.marshalNString2string(ctx, field.Selections, res)
}
func (ec *executionContext) _PhotoDownload_width(ctx context.Context, field graphql.CollectedField, obj *models.PhotoDownload) (ret graphql.Marshaler) {
ctx = ec.Tracer.StartFieldExecution(ctx, field)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
ec.Tracer.EndFieldExecution(ctx)
}()
rctx := &graphql.ResolverContext{
Object: "PhotoDownload",
Field: field,
Args: nil,
IsMethod: false,
}
ctx = graphql.WithResolverContext(ctx, rctx)
ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Width, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !ec.HasError(rctx) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(int)
rctx.Result = res
ctx = ec.Tracer.StartFieldChildExecution(ctx)
return ec.marshalNInt2int(ctx, field.Selections, res)
}
func (ec *executionContext) _PhotoDownload_height(ctx context.Context, field graphql.CollectedField, obj *models.PhotoDownload) (ret graphql.Marshaler) {
ctx = ec.Tracer.StartFieldExecution(ctx, field)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
ec.Tracer.EndFieldExecution(ctx)
}()
rctx := &graphql.ResolverContext{
Object: "PhotoDownload",
Field: field,
Args: nil,
IsMethod: false,
}
ctx = graphql.WithResolverContext(ctx, rctx)
ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Height, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !ec.HasError(rctx) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(int)
rctx.Result = res
ctx = ec.Tracer.StartFieldChildExecution(ctx)
return ec.marshalNInt2int(ctx, field.Selections, res)
}
func (ec *executionContext) _PhotoDownload_url(ctx context.Context, field graphql.CollectedField, obj *models.PhotoDownload) (ret graphql.Marshaler) {
ctx = ec.Tracer.StartFieldExecution(ctx, field)
defer func() {
@ -6402,6 +6494,16 @@ func (ec *executionContext) _PhotoDownload(ctx context.Context, sel ast.Selectio
if out.Values[i] == graphql.Null {
invalids++
}
case "width":
out.Values[i] = ec._PhotoDownload_width(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "height":
out.Values[i] = ec._PhotoDownload_height(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "url":
out.Values[i] = ec._PhotoDownload_url(ctx, field, obj)
if out.Values[i] == graphql.Null {

View File

@ -33,8 +33,10 @@ type Notification struct {
}
type PhotoDownload struct {
Title string `json:"title"`
URL string `json:"url"`
Title string `json:"title"`
Width int `json:"width"`
Height int `json:"height"`
URL string `json:"url"`
}
// EXIF metadata from the camera

View File

@ -79,3 +79,17 @@ func NewPhotoURLFromRow(row *sql.Row) (*PhotoURL, error) {
return &url, nil
}
func NewPhotoURLFromRows(rows *sql.Rows) ([]*PhotoURL, error) {
urls := make([]*PhotoURL, 0)
for rows.Next() {
var url PhotoURL
if err := rows.Scan(&url.UrlID, &url.PhotoId, &url.PhotoName, &url.Width, &url.Height, &url.Purpose, &url.ContentType); err != nil {
return nil, err
}
urls = append(urls, &url)
}
return urls, nil
}

View File

@ -69,8 +69,39 @@ func (r *photoResolver) Shares(ctx context.Context, obj *models.Photo) ([]*model
}
func (r *photoResolver) Downloads(ctx context.Context, obj *models.Photo) ([]*models.PhotoDownload, error) {
log.Println("Photo: downloads not implemented")
rows, err := r.Database.Query("SELECT * FROM photo_url WHERE photo_id = ?", obj.PhotoID)
if err != nil {
return nil, err
}
photoUrls, err := models.NewPhotoURLFromRows(rows)
if err != nil {
return nil, err
}
downloads := make([]*models.PhotoDownload, 0)
for _, url := range photoUrls {
var title string
switch {
case url.Purpose == models.PhotoOriginal:
title = "Original"
case url.Purpose == models.PhotoThumbnail:
title = "Small"
case url.Purpose == models.PhotoHighRes:
title = "Large"
}
downloads = append(downloads, &models.PhotoDownload{
Title: title,
Width: url.Width,
Height: url.Height,
URL: url.URL(),
})
}
return downloads, nil
}

View File

@ -177,6 +177,8 @@ type PhotoURL {
type PhotoDownload {
title: String!
width: Int!
height: Int!
url: String!
}

View File

@ -14,11 +14,13 @@ import (
func CORSMiddleware(devMode bool) mux.MiddlewareFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
methods := []string{http.MethodGet, http.MethodPost, http.MethodOptions}
headers := []string{"authorization", "content-type"}
headers := []string{"authorization", "content-type", "content-length"}
w.Header().Set("Access-Control-Allow-Methods", strings.Join(methods, ","))
w.Header().Set("Access-Control-Allow-Headers", strings.Join(headers, ","))
w.Header().Set("Access-Control-Expose-Headers", "content-length")
endpoint, err := url.Parse(os.Getenv("API_ENDPOINT"))
if err != nil {

View File

@ -46,6 +46,12 @@ const tokenQuery = gql`
width
height
}
downloads {
title
url
width
height
}
highRes {
url
}

View File

@ -59,7 +59,7 @@ const linkError = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors.length == 1) {
errorMessages.push({
header: 'Something went wrong',
content: graphQLErrors[0].message,
content: `Server error: ${graphQLErrors[0].message} at (${graphQLErrors[0].path})`,
})
} else if (graphQLErrors.length > 1) {
errorMessages.push({

View File

@ -17,17 +17,22 @@ export let MessageState = {
set: null,
get: null,
add: message => {
MessageState.set(messages => [...messages, message])
MessageState.set(messages => {
const newMessages = messages.filter(msg => msg.key != message.key)
newMessages.push(message)
return newMessages
})
},
removeKey: key => {
MessageState.set(messages => {
const newMessages = messages.filter(msg => msg.key != key)
return newMessages
})
},
}
const Messages = () => {
if (!localStorage.getItem('token')) {
return null
}
console.log('Rendering messages')
const [messages, setMessages] = useState([])
MessageState.set = setMessages
MessageState.get = messages
@ -76,7 +81,6 @@ const Messages = () => {
resolveFunc = resolve
})
console.log(resolveFunc, waitPromise)
refHooks.set(message.key, {
done: resolveFunc,
promise: waitPromise,
@ -89,13 +93,8 @@ const Messages = () => {
height: '0px',
},
enter: item => async next => {
console.log('HERE', refMap, item)
const refPromise = refHooks.get(item.key).promise
console.log('promise', refPromise)
await refPromise
console.log('AFTER PROMISE', refMap, item)
await next({
opacity: 1,
@ -109,7 +108,6 @@ const Messages = () => {
<Container>
{transitions.map(({ item, props: style, key }) => {
const getRef = ref => {
console.log('GET REF', refMap, refHooks, item.key)
refMap.set(item, ref)
if (refHooks.has(item.key)) {
refHooks.get(item.key).done()

View File

@ -18,6 +18,10 @@ const notificationSubscription = gql`
`
const SubscriptionsHook = ({ messages, setMessages }) => {
if (!localStorage.getItem('token')) {
return null
}
const { data, error } = useSubscription(notificationSubscription)
useEffect(() => {
@ -62,34 +66,6 @@ const SubscriptionsHook = ({ messages, setMessages }) => {
newMessages.push(newNotification)
}
// const update = data.scannerStatusUpdate
// if (update.success) {
// newMessages[0] = {
// key: 'primary',
// type: 'progress',
// props: {
// header: update.finished ? 'Synced' : 'Syncing',
// content: update.message,
// percent: update.progress,
// positive: update.finished,
// },
// }
// if (!update.finished) newMessages[0].props.onDismiss = null
// } else {
// const key = Math.random().toString(26)
// newMessages.push({
// key,
// type: 'message',
// props: {
// header: 'Sync error',
// content: update.message,
// negative: true,
// },
// })
// }
setMessages(newMessages)
}, [data, error])

View File

@ -48,7 +48,6 @@ const ProtectedImage = ({ src, ...props }) => {
// Get share token if not authorized
const token = location.pathname.match(/^\/share\/([\d\w]+)(\/?.*)$/)
console.log('share token', location.pathname, token)
if (token) {
imgUrl.searchParams.set('token', token[1])
}

View File

@ -93,7 +93,7 @@ const SidebarContent = ({ photo, hidePreview }) => {
{!hidePreview && <PreviewImage src={previewUrl} />}
<Name>{photo && photo.title}</Name>
<div>{exifItems}</div>
<SidebarDownload photoId={photo.id} />
<SidebarDownload photo={photo} />
<SidebarShare photo={photo} />
</div>
)

View File

@ -1,7 +1,8 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Menu, Dropdown, Button } from 'semantic-ui-react'
import { Query } from 'react-apollo'
import { MessageState } from '../messages/Messages'
import { Query, useLazyQuery } from 'react-apollo'
import gql from 'graphql-tag'
import download from 'downloadjs'
@ -12,49 +13,157 @@ const downloadQuery = gql`
downloads {
title
url
width
height
}
}
}
`
function formatBytes(bytes) {
var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
if (bytes == 0) return '0 Byte'
var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)))
return Math.round(bytes / Math.pow(1024, i), 2) + ' ' + sizes[i]
}
const downloadPhoto = async url => {
const request = await fetch(url, {
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
const imgUrl = new URL(url)
let headers = {
Authorization: `Bearer ${localStorage.getItem('token')}`,
}
if (localStorage.getItem('token') == null) {
// Get share token if not authorized
const token = location.pathname.match(/^\/share\/([\d\w]+)(\/?.*)$/)
if (token) {
imgUrl.searchParams.set('token', token[1])
}
headers = {}
}
const response = await fetch(imgUrl.href, {
headers,
})
const totalBytes = Number(response.headers.get('content-length'))
console.log(totalBytes)
if (totalBytes == 0) {
MessageState.add({
key: Math.random().toString(26),
type: 'message',
props: {
header: 'Error downloading photo',
content: `Could not get size of photo from server`,
negative: true,
},
})
return
}
const notifKey = Math.random().toString(26)
MessageState.add({
key: notifKey,
type: 'progress',
props: {
header: 'Downloading photo',
content: `Starting download`,
progress: 0,
},
})
const content = await request.blob()
const reader = response.body.getReader()
let data = new Uint8Array(totalBytes)
let receivedBytes = 0
let result
do {
result = await reader.read()
if (result.value) data.set(result.value, receivedBytes)
receivedBytes += result.value ? result.value.length : 0
MessageState.add({
key: notifKey,
type: 'progress',
props: {
header: 'Downloading photo',
percent: (receivedBytes / totalBytes) * 100,
content: `${formatBytes(receivedBytes)} of ${formatBytes(
totalBytes
)} bytes downloaded`,
},
})
} while (!result.done)
MessageState.add({
key: notifKey,
type: 'progress',
props: {
header: 'Downloading photo completed',
content: `The photo has been downloaded`,
percent: 100,
positive: true,
},
})
setTimeout(() => {
MessageState.removeKey(notifKey)
}, 2000)
const content = new Blob([data.buffer], {
type: response.headers.get('content-type'),
})
const filename = url.match(/[^/]*$/)[0]
download(content, filename)
}
const SidebarDownload = ({ photoId }) => {
if (!photoId) return null
const SidebarDownload = ({ photo }) => {
if (!photo || !photo.id) return null
const [
loadPhotoDownloads,
{ called, loading, data },
] = useLazyQuery(downloadQuery, { variables: { photoId: photo.id } })
let downloads = []
if (called) {
if (!loading) {
downloads = data && data.photo.downloads
}
} else {
if (!photo.downloads) {
loadPhotoDownloads()
} else {
downloads = photo.downloads
}
}
let buttons = downloads.map(x => (
<Button
style={{ marginTop: 4 }}
key={x.url}
onClick={() => downloadPhoto(x.url)}
>
{`${x.title} (${x.width} x ${x.height})`}
</Button>
))
return (
<div style={{ marginBottom: 24 }}>
<h2>Download</h2>
<Query query={downloadQuery} variables={{ photoId }}>
{({ loading, error, data }) => {
if (error) return <div>Error {error.message}</div>
if (!data || !data.photo) return null
let buttons = data.photo.downloads.map(x => (
<Button key={x.url} onClick={() => downloadPhoto(x.url)}>
{x.title}
</Button>
))
return <Button.Group>{buttons}</Button.Group>
}}
</Query>
<div>{buttons}</div>
</div>
)
}
SidebarDownload.propTypes = {
photoId: PropTypes.number,
photo: PropTypes.object,
}
export default SidebarDownload