Skip to content

Commit

Permalink
feat: support CAR uploads (#620)
Browse files Browse the repository at this point in the history
w3up-client has a `Client#uploadCAR` function - add support for this to
the `Uploader`
  • Loading branch information
travis authored Jan 11, 2024
1 parent 2afc05c commit 57ebba2
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 8 deletions.
1 change: 1 addition & 0 deletions eslint.packages.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ module.exports = {
'unicorn/filename-case': 'off',
'unicorn/no-useless-undefined': 'off',
'unicorn/expiring-todo-comments': 'off',
'unicorn/no-nested-ternary': 'off',
'jsdoc/require-param': 'off',
'jsdoc/newline-after-description': 'off',
'jsdoc/require-param-type': 'off',
Expand Down
46 changes: 38 additions & 8 deletions packages/react/src/Uploader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,16 @@ export interface UploaderContextState {
uploadProgress: UploadProgress
/**
* Should single files be wrapped in a directory?
*
* If uploadAsCAR is true, this option is ignored.
*/
wrapInDirectory: boolean
/**
* Should the file be uploaded as a CAR?
*
* This option is ignored if more than one file is being uploaded.
*/
uploadAsCAR: boolean
}

export interface UploaderContextActions {
Expand All @@ -75,8 +83,16 @@ export interface UploaderContextActions {
setFiles: (file?: File[]) => void
/**
* Set whether single files should be wrapped in a directory before upload.
*
* If uploadAsCAR is true, this option is ignored.
*/
setWrapInDirectory: (wrap: boolean) => void
/**
* Set whether single files should be uploaded as a CAR.
*
* This option is ignored if more than one file is being uploaded.
*/
setUploadAsCAR: (uploadAsCar: boolean) => void
}

export type UploaderContextValue = [
Expand All @@ -89,7 +105,8 @@ export const UploaderContextDefaultValue: UploaderContextValue = [
status: UploadStatus.Idle,
storedDAGShards: [],
uploadProgress: {},
wrapInDirectory: false
wrapInDirectory: false,
uploadAsCAR: false
},
{
setFile: () => {
Expand All @@ -100,6 +117,9 @@ export const UploaderContextDefaultValue: UploaderContextValue = [
},
setWrapInDirectory: () => {
throw new Error('missing set wrap in directory function')
},
setUploadAsCAR: () => {
throw new Error('missing set upload as CAR function')
}
}
]
Expand All @@ -117,6 +137,7 @@ export type OnUploadComplete = (props: OnUploadCompleteProps) => void
export type UploaderRootOptions<T extends As = typeof Fragment> = Options<T> & {
onUploadComplete?: OnUploadComplete
defaultWrapInDirectory?: boolean
defaultUploadAsCAR?: boolean
}
export type UploaderRootProps<T extends As = typeof Fragment> = Props<UploaderRootOptions<T>>

Expand All @@ -128,12 +149,13 @@ export type UploaderRootProps<T extends As = typeof Fragment> = Props<UploaderRo
* web3.storage.
*/
export const UploaderRoot: Component<UploaderRootProps> = createComponent(
({ onUploadComplete, defaultWrapInDirectory = false, ...props }) => {
({ onUploadComplete, defaultWrapInDirectory = false, defaultUploadAsCAR = false, ...props }) => {
const [{ client }] = useW3()
const [files, setFiles] = useState<File[]>()
const file = files?.[0]
const setFile = (file: File | undefined): void => { (file != null) && setFiles([file]) }
const [wrapInDirectory, setWrapInDirectory] = useState(defaultWrapInDirectory)
const [uploadAsCAR, setUploadAsCAR] = useState(defaultUploadAsCAR)
const [dataCID, setDataCID] = useState<AnyLink>()
const [status, setStatus] = useState(UploadStatus.Idle)
const [error, setError] = useState()
Expand Down Expand Up @@ -165,9 +187,11 @@ export const UploaderRoot: Component<UploaderRootProps> = createComponent(
}
const cid = files.length > 1
? await client.uploadDirectory(files, uploadOptions)
: (wrapInDirectory
? await client.uploadDirectory(files, uploadOptions)
: await client.uploadFile(file, uploadOptions))
: (uploadAsCAR
? await client.uploadCAR(file, uploadOptions)
: (wrapInDirectory
? await client.uploadDirectory(files, uploadOptions)
: await client.uploadFile(file, uploadOptions)))

setDataCID(cid)
setStatus(UploadStatus.Succeeded)
Expand All @@ -193,12 +217,14 @@ export const UploaderRoot: Component<UploaderRootProps> = createComponent(
handleUploadSubmit,
storedDAGShards,
uploadProgress,
wrapInDirectory
wrapInDirectory,
uploadAsCAR
},
{
setFile: (file?: File) => { setFilesAndReset((file === undefined) ? file : [file]) },
setFiles: setFilesAndReset,
setWrapInDirectory
setWrapInDirectory,
setUploadAsCAR
}
],
[
Expand Down Expand Up @@ -231,7 +257,7 @@ export type UploaderInputProps<T extends As = 'input'> = Props<UploaderInputOpti
* be passed along to the `input` component.
*/
export const UploaderInput: Component<UploaderInputProps> = createComponent(({ allowDirectory, ...props }) => {
const [, { setFiles }] = useContext(UploaderContext)
const [{ uploadAsCAR }, { setFiles }] = useContext(UploaderContext)
const onChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
if (e.target.files != null) {
Expand All @@ -246,6 +272,10 @@ export const UploaderInput: Component<UploaderInputProps> = createComponent(({ a
// set at all seems to be the only way to get it working the way you'd expect
inputProps.webkitdirectory = 'true'
}
const acceptNotSet = (inputProps.accept === undefined)
if (uploadAsCAR && acceptNotSet) {
inputProps.accept = '.car'
}
return createElement('input', inputProps)
})

Expand Down
38 changes: 38 additions & 0 deletions packages/react/test/Uploader.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,41 @@ test('wrapping a file in a directory', async () => {

expect(client.uploadDirectory).toHaveBeenCalled()
})

test('uploading a CAR directly', async () => {
const cid = Link.parse('bafybeibrqc2se2p3k4kfdwg7deigdggamlumemkiggrnqw3edrjosqhvnm')
const client = {
uploadCAR: vi.fn().mockImplementation(() => cid)
}

const contextValue: ContextValue = [
{
...ContextDefaultValue[0],
// @ts-expect-error not a real client
client
},
ContextDefaultValue[1]
]
const handleComplete = vi.fn()
render(
<Context.Provider value={contextValue}>
<Uploader onUploadComplete={handleComplete} defaultUploadAsCAR>
<Uploader.Form>
<Uploader.Input data-testid='file-upload' />
<input type='submit' value='Upload' />
</Uploader.Form>
</Uploader>
</Context.Provider>
)

// this isn't a real CAR but that's probably ok for a test
const file = new File(['hello'], 'hello.car', { type: 'application/vnd.ipld.car' })

const fileInput = screen.getByTestId('file-upload')
await user.upload(fileInput, file)

const submitButton = screen.getByText('Upload')
await user.click(submitButton)

expect(client.uploadCAR).toHaveBeenCalled()
})

0 comments on commit 57ebba2

Please sign in to comment.