image uploading backend

This commit is contained in:
keyan 2022-05-12 13:44:21 -05:00
parent e2409efbaf
commit 9abc41b7b2
12 changed files with 267 additions and 8 deletions

View File

@ -6,5 +6,8 @@ import lnurl from './lnurl'
import notifications from './notifications'
import invite from './invite'
import sub from './sub'
import upload from './upload'
import { GraphQLJSONObject } from 'graphql-type-json'
export default [user, item, message, wallet, lnurl, notifications, invite, sub]
export default [user, item, message, wallet, lnurl, notifications, invite, sub,
upload, { JSONObject: GraphQLJSONObject }]

61
api/resolvers/upload.js Normal file
View File

@ -0,0 +1,61 @@
import { AuthenticationError, UserInputError } from 'apollo-server-micro'
import AWS from 'aws-sdk'
import { IMAGE_PIXELS_MAX, UPLOAD_SIZE_MAX, UPLOAD_TYPES_ALLOW } from '../../lib/constants'
const bucketRegion = 'us-east-1'
AWS.config.update({
region: bucketRegion
})
export default {
Mutation: {
getSignedPOST: async (parent, { type, size, width, height }, { models, me }) => {
if (!me) {
throw new AuthenticationError('you must be logged in to get a signed url')
}
if (UPLOAD_TYPES_ALLOW.indexOf(type) === -1) {
throw new UserInputError(`image must be ${UPLOAD_TYPES_ALLOW.map(t => t.replace('image/', '')).join(', ')}`)
}
if (size > UPLOAD_SIZE_MAX) {
throw new UserInputError(`image must be less than ${UPLOAD_SIZE_MAX} bytes`)
}
if (width * height > IMAGE_PIXELS_MAX) {
throw new UserInputError(`image must be less than ${IMAGE_PIXELS_MAX} pixels`)
}
// create upload record
const upload = await models.upload.create({
data: {
type,
size,
width,
height,
userId: me.id
}
})
// get presigned POST ur
const s3 = new AWS.S3({ apiVersion: '2006-03-01' })
const res = await new Promise((resolve, reject) => {
s3.createPresignedPost({
Bucket: process.env.AWS_UPLOAD_BUCKET,
Fields: {
key: String(upload.id)
},
Expires: 300,
Conditions: [
{ 'Content-Type': type },
{ acl: 'public-read' },
['content-length-range', size, size]
]
}, (err, preSigned) => { if (err) { reject(err) } else { resolve(preSigned) } })
})
return res
}
}
}

View File

@ -8,6 +8,7 @@ import lnurl from './lnurl'
import notifications from './notifications'
import invite from './invite'
import sub from './sub'
import upload from './upload'
const link = gql`
type Query {
@ -23,4 +24,5 @@ const link = gql`
}
`
export default [link, user, item, message, wallet, lnurl, notifications, invite, sub]
export default [link, user, item, message, wallet, lnurl, notifications, invite,
sub, upload]

14
api/typeDefs/upload.js Normal file
View File

@ -0,0 +1,14 @@
import { gql } from 'apollo-server-micro'
export default gql`
scalar JSONObject
extend type Mutation {
getSignedPOST(type: String!, size: Int!, width: Int!, height: Int!): SignedPost!
}
type SignedPost {
url: String!
fields: JSONObject!
}
`

View File

@ -45,10 +45,6 @@
border: 1px solid var(--theme-body);
}
.navLink:last-child {
padding-right: 0 !important;
}
.navbarNav {
width: 100%;
flex-wrap: wrap;

118
components/upload.js Normal file
View File

@ -0,0 +1,118 @@
import { useRef, useState } from 'react'
import { gql, useMutation } from '@apollo/client'
import { UPLOAD_TYPES_ALLOW } from '../lib/constants'
import AddImage from '../svgs/image-add-fill.svg'
import Moon from '../svgs/moon-fill.svg'
import { Image as BImage } from 'react-bootstrap'
// what we want is that they supply a component that we turn into an upload button
// we need to return an error if the upload fails
// we need to report that the upload started
// we return the image id on success
export default function Upload ({ as: Component, onStarted, onError, onSuccess }) {
const [getSignedPOST] = useMutation(
gql`
mutation getSignedPOST($type: String!, $size: Int!, $width: Int!, $height: Int!) {
getSignedPOST(type: $type, size: $size, width: $width, height: $height) {
url
fields
}
}`)
const ref = useRef()
return (
<>
<input
ref={ref}
type='file'
className='d-none'
onChange={(e) => {
if (e.target.files.length === 0) {
return
}
onStarted && onStarted()
const file = e.target.files[0]
if (UPLOAD_TYPES_ALLOW.indexOf(file.type) === -1) {
onError && onError(`image must be ${UPLOAD_TYPES_ALLOW.map(t => t.replace('image/', '')).join(', ')}`)
return
}
const img = new Image()
img.src = window.URL.createObjectURL(file)
img.onload = async () => {
let data
try {
({ data } = await getSignedPOST({
variables: {
type: file.type,
size: file.size,
width: img.width,
height: img.height
}
}))
} catch (e) {
onError && onError(e.toString())
return
}
const form = new FormData()
Object.keys(data.getSignedPOST.fields).forEach(key =>
form.append(key, data.getSignedPOST.fields[key]))
form.append('Content-Type', file.type)
form.append('acl', 'public-read')
form.append('file', file)
const res = await fetch(data.getSignedPOST.url, {
method: 'POST',
body: form
})
if (!res.ok) {
onError && onError(res.statusText)
return
}
onSuccess && onSuccess(data.getSignedPOST.fields.key)
}
}}
/>
<Component onClick={() => ref.current?.click()} />
</>
)
}
export function UploadExample () {
const [error, setError] = useState()
const [key, setKey] = useState()
const [uploading, setUploading] = useState()
const Component = uploading
? ({ onClick }) => <Moon className='fill-grey spin' onClick={onClick} />
: ({ onClick }) => <AddImage className='fill-grey' onClick={onClick} />
return (
<>
<Upload
onError={e => {
setUploading(false)
setError(e)
}}
onSuccess={key => {
setUploading(false)
setKey(key)
}}
onStarted={() => {
setError(false)
setUploading(true)
}}
as={Component}
/>
<div>
{key && <BImage src={`https://sn-mtest.s3.amazonaws.com/${key}`} width='100%' />}
{error && <div>{error}</div>}
</div>
</>
)
}

View File

@ -1,2 +1,11 @@
export const NOFOLLOW_LIMIT = 100
export const BOOST_MIN = 1000
export const UPLOAD_SIZE_MAX = 2 * 1024 * 1024
export const IMAGE_PIXELS_MAX = 35000000
export const UPLOAD_TYPES_ALLOW = [
'image/gif',
'image/heic',
'image/png',
'image/jpeg',
'image/webp'
]

5
package-lock.json generated
View File

@ -3764,6 +3764,11 @@
}
}
},
"graphql-type-json": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/graphql-type-json/-/graphql-type-json-0.3.2.tgz",
"integrity": "sha512-J+vjof74oMlCWXSvt0DOf2APEdZOCdubEvGDUAlqH//VBYcOYsGgRW7Xzorr44LvkjiuvecWc8fChxuZZbChtg=="
},
"has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",

View File

@ -24,6 +24,7 @@
"domino": "^2.1.6",
"formik": "^2.2.6",
"graphql": "^15.5.0",
"graphql-type-json": "^0.3.2",
"ln-service": "^52.8.0",
"mdast-util-find-and-replace": "^1.1.1",
"next": "^11.1.2",

View File

@ -0,0 +1,29 @@
-- CreateTable
CREATE TABLE "Upload" (
"id" SERIAL NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"type" TEXT NOT NULL,
"size" INTEGER NOT NULL,
"width" INTEGER,
"height" INTEGER,
"itemId" INTEGER,
"userId" INTEGER NOT NULL,
PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "Upload.created_at_index" ON "Upload"("created_at");
-- CreateIndex
CREATE INDEX "Upload.itemId_index" ON "Upload"("itemId");
-- CreateIndex
CREATE INDEX "Upload.userId_index" ON "Upload"("userId");
-- AddForeignKey
ALTER TABLE "Upload" ADD FOREIGN KEY ("itemId") REFERENCES "Item"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Upload" ADD FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -53,12 +53,31 @@ model User {
noteInvites Boolean @default(true)
noteJobIndicator Boolean @default(true)
Earn Earn[]
Earn Earn[]
Upload Upload[]
@@index([createdAt])
@@index([inviteId])
@@map(name: "users")
}
model Upload {
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @default(now()) @updatedAt @map(name: "updated_at")
type String
size Int
width Int?
height Int?
item Item? @relation(fields: [itemId], references: [id])
itemId Int?
user User @relation(fields: [userId], references: [id])
userId Int
@@index([createdAt])
@@index([itemId])
@@index([userId])
}
model Earn {
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map(name: "created_at")
@ -153,7 +172,8 @@ model Item {
longitude Float?
remote Boolean?
User User[]
User User[]
Upload Upload[]
@@index([createdAt])
@@index([userId])
@@index([parentId])

1
svgs/image-add-fill.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M21 15v3h3v2h-3v3h-2v-3h-3v-2h3v-3h2zm.008-12c.548 0 .992.445.992.993v9.349A5.99 5.99 0 0 0 20 13V5H4l.001 14 9.292-9.293a.999.999 0 0 1 1.32-.084l.093.085 3.546 3.55a6.003 6.003 0 0 0-3.91 7.743L2.992 21A.993.993 0 0 1 2 20.007V3.993A1 1 0 0 1 2.992 3h18.016zM8 7a2 2 0 1 1 0 4 2 2 0 0 1 0-4z"/></svg>

After

Width:  |  Height:  |  Size: 431 B