Add download support
This commit is contained in:
parent
37aea1fb3a
commit
e00a5553f7
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -177,6 +177,8 @@ type PhotoURL {
|
|||
|
||||
type PhotoDownload {
|
||||
title: String!
|
||||
width: Int!
|
||||
height: Int!
|
||||
url: String!
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -46,6 +46,12 @@ const tokenQuery = gql`
|
|||
width
|
||||
height
|
||||
}
|
||||
downloads {
|
||||
title
|
||||
url
|
||||
width
|
||||
height
|
||||
}
|
||||
highRes {
|
||||
url
|
||||
}
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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])
|
||||
|
||||
|
|
|
@ -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])
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue