feat: add metadata and preview (#292)
* chore: upload flow uses metadata object and has preview * chore: remove SwarmFile * feat: upload metadata and file preview * feat: add metadata and preview on download * fix: package the meta and preview files * fix: upload websites that are inside a folder (#296) * fix: upload websites that are inside a folder * docs: few comments to clarify what is going on * refactor: decrease local variables and fix state order to detect websites properly Co-authored-by: Cafe137 <aron@aronsoos.com>
This commit is contained in:
@@ -1,24 +0,0 @@
|
||||
export class SwarmFile {
|
||||
public name: string
|
||||
public path: string
|
||||
public type: string
|
||||
public size: number
|
||||
public webkitRelativePath: string
|
||||
public arrayBuffer: () => Promise<ArrayBuffer>
|
||||
private data: Promise<ArrayBuffer>
|
||||
|
||||
constructor(file: File) {
|
||||
const path = Reflect.get(file, 'path') || file.webkitRelativePath || file.name
|
||||
this.path = path.startsWith('/') ? path.slice(1) : path
|
||||
this.webkitRelativePath = this.path
|
||||
this.name = file.name
|
||||
this.type = file.type
|
||||
this.size = file.size
|
||||
this.data = file.arrayBuffer()
|
||||
this.arrayBuffer = async () => {
|
||||
const data = await this.data
|
||||
|
||||
return data
|
||||
}
|
||||
}
|
||||
}
|
||||
+53
-37
@@ -1,28 +1,32 @@
|
||||
import { FileData } from '@ethersphere/bee-js'
|
||||
import { SwarmFile } from './SwarmFile'
|
||||
|
||||
const indexHtmls = ['index.html', 'index.htm']
|
||||
|
||||
export function detectIndexHtml(files: SwarmFile[]): string | false {
|
||||
if (!files.length) {
|
||||
interface DetectedIndex {
|
||||
indexPath: string
|
||||
commonPrefix?: string
|
||||
}
|
||||
|
||||
export function detectIndexHtml(files: FilePath[]): DetectedIndex | false {
|
||||
const paths = files.map(getPath)
|
||||
|
||||
if (!paths.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
const exactMatch = files.find(x => indexHtmls.includes(x.path))
|
||||
const exactMatch = paths.find(x => indexHtmls.includes(x))
|
||||
|
||||
if (exactMatch) {
|
||||
return exactMatch.name
|
||||
return { indexPath: exactMatch }
|
||||
}
|
||||
|
||||
const prefix = files[0].path.split('/')[0] + '/'
|
||||
const prefix = paths[0].split('/')[0] + '/'
|
||||
|
||||
const allStartWithSamePrefix = files.every(x => x.path.startsWith(prefix))
|
||||
const allStartWithSamePrefix = paths.every(x => x.startsWith(prefix))
|
||||
|
||||
if (allStartWithSamePrefix) {
|
||||
const match = files.find(x => indexHtmls.map(y => prefix + y).includes(x.path))
|
||||
const match = paths.find(x => indexHtmls.map(y => prefix + y).includes(x))
|
||||
|
||||
if (match) {
|
||||
return match.name
|
||||
return { indexPath: match, commonPrefix: prefix }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,38 +57,50 @@ export function getHumanReadableFileSize(bytes: number): string {
|
||||
return bytes + ' bytes'
|
||||
}
|
||||
|
||||
export function convertBeeFileToBrowserFile(file: FileData<ArrayBuffer>): Partial<File> {
|
||||
return {
|
||||
name: file.name,
|
||||
size: file.data.byteLength,
|
||||
type: file.contentType,
|
||||
arrayBuffer: () => new Promise(resolve => resolve(file.data)),
|
||||
}
|
||||
}
|
||||
|
||||
export function convertManifestToFiles(files: Record<string, string>): SwarmFile[] {
|
||||
return Object.entries(files).map(
|
||||
x =>
|
||||
({
|
||||
name: x[0],
|
||||
path: x[0],
|
||||
type: 'n/a',
|
||||
size: 0,
|
||||
webkitRelativePath: x[0],
|
||||
arrayBuffer: () => new Promise(resolve => resolve(new ArrayBuffer(0))),
|
||||
} as SwarmFile),
|
||||
)
|
||||
}
|
||||
|
||||
export function getAssetNameFromFiles(files: SwarmFile[]): string {
|
||||
export function getAssetNameFromFiles(files: FilePath[]): string {
|
||||
if (files.length === 1) return files[0].name
|
||||
|
||||
if (files.length > 0) {
|
||||
const prefix = files[0].path.split('/')[0]
|
||||
const prefix = getPath(files[0]).split('/')[0]
|
||||
|
||||
// Only if all files have a common prefix we can use it as a folder name
|
||||
if (files.every(f => f.path.split('/')[0] === prefix)) return prefix
|
||||
if (files.every(f => getPath(f).split('/')[0] === prefix)) return prefix
|
||||
}
|
||||
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
export function getMetadata(files: FilePath[]): Metadata {
|
||||
const size = files.reduce((total, item) => total + item.size, 0)
|
||||
const isWebsite = Boolean(detectIndexHtml(files))
|
||||
const name = getAssetNameFromFiles(files)
|
||||
const type = files.length === 1 ? files[0].type : 'folder'
|
||||
const count = files.length
|
||||
|
||||
return { size, name, type, isWebsite, count }
|
||||
}
|
||||
|
||||
export function getPath(file: FilePath): string {
|
||||
return (file.path || file.webkitRelativePath || file.name).replace(/^\//g, '') // remove the starting slash
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function that is needed to have correct directory structure as webkitRelativePath is read only
|
||||
*/
|
||||
export function packageFile(file: FilePath): FilePath {
|
||||
const path = getPath(file)
|
||||
|
||||
return {
|
||||
path: path,
|
||||
fullPath: path,
|
||||
webkitRelativePath: path,
|
||||
lastModified: file.lastModified,
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
stream: file.stream,
|
||||
slice: file.slice,
|
||||
text: file.text,
|
||||
arrayBuffer: async () => await file.arrayBuffer(), // This is needed for successful upload and can not simply be { arrayBuffer: file.arrayBuffer }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
interface Dimensions {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the dimensions of the image after resize
|
||||
*
|
||||
* @param imgWidth Current image width
|
||||
* @param imgHeight Current image height
|
||||
* @param maxWidth Desired max width
|
||||
* @param maxHeight Desired max height
|
||||
*
|
||||
* @returns Downscaled dimensions of the image to fit in the bounding box
|
||||
*/
|
||||
export function getDimensions(imgWidth: number, imgHeight: number, maxWidth?: number, maxHeight?: number): Dimensions {
|
||||
const ratioWidth = maxWidth ? imgWidth / maxWidth : 1
|
||||
const ratioHeight = maxHeight ? imgHeight / maxHeight : 1
|
||||
|
||||
const ratio = Math.max(ratioWidth, ratioHeight)
|
||||
|
||||
// No need to resize
|
||||
if (ratio <= 1) return { width: imgWidth, height: imgHeight }
|
||||
|
||||
return { width: imgWidth / ratio, height: imgHeight / ratio }
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize image passed to fit in the bounding box defined with maxWidth and maxHeight.
|
||||
* Note that one or both of the bounding box dimensions may be omitted
|
||||
*
|
||||
* @param file Image file to be resized
|
||||
* @param maxWidth Maximal image width
|
||||
* @param maxHeight Maximal image height
|
||||
*
|
||||
* @returns Promise that resolves into the resized image blob
|
||||
*/
|
||||
export function resize(file: File, maxWidth?: number, maxHeight?: number): Promise<Blob> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const allowedTypes = [
|
||||
'image/bmp',
|
||||
'image/gif',
|
||||
'image/vnd.microsoft.icon',
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/svg+xml',
|
||||
'image/tiff',
|
||||
'image/webp',
|
||||
]
|
||||
|
||||
if (!file.size || !file.type || !allowedTypes.includes(file.type)) return reject('File not supported!')
|
||||
|
||||
try {
|
||||
const reader = new FileReader()
|
||||
reader.readAsDataURL(file)
|
||||
reader.onload = event => {
|
||||
const src = event?.target?.result
|
||||
|
||||
if (!src || typeof src !== 'string') throw new Error('Failed to load the image source')
|
||||
|
||||
const img = new Image()
|
||||
img.src = src
|
||||
img.onload = () => {
|
||||
const dimensions = getDimensions(img.width, img.height, maxWidth, maxHeight)
|
||||
const elem = document.createElement('canvas')
|
||||
elem.width = dimensions.width
|
||||
elem.height = dimensions.height
|
||||
const ctx = elem.getContext('2d')
|
||||
|
||||
if (!ctx) throw new Error('Failed to create canvas context')
|
||||
|
||||
ctx.drawImage(img, 0, 0, elem.width, elem.height)
|
||||
ctx.canvas.toBlob(
|
||||
blob => {
|
||||
if (!blob) throw new Error('Failed to extract the blob from canvas')
|
||||
|
||||
resolve(blob)
|
||||
},
|
||||
'image/jpeg',
|
||||
1,
|
||||
)
|
||||
}
|
||||
}
|
||||
reader.onerror = error => reject(error)
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user