More logic for upload/download

pull/152/head
Alex Kern 5 years ago
parent 981bd31253
commit a0a1d8643c
No known key found for this signature in database
GPG Key ID: F3141D5EDF48F89F

@ -1,20 +0,0 @@
workflow "Build on push" {
on = "push"
resolves = ["AWS deploy"]
}
action "Docker build, tag, and push" {
uses = "pangzineng/Github-Action-One-Click-Docker@master"
secrets = ["DOCKER_USERNAME", "DOCKER_PASSWORD"]
}
action "AWS deploy" {
uses = "actions/aws/cli@efb074ae4510f2d12c7801e4461b65bf5e8317e6"
needs = ["Docker build, tag, and push"]
secrets = [
"AWS_ACCESS_KEY_ID",
"AWS_SECRET_ACCESS_KEY",
]
args = "deploy --region us-west-2 create-deployment --application-name AppECS-filepizza-filepizza --deployment-config-name CodeDeployDefault.ECSAllAtOnce --deployment-group-name DgpECS-filepizza-filepizza --github-location repository=kern/filepizza,commitId=$GITHUB_REF"
runs = "aws"
}

@ -20,16 +20,21 @@
"dependencies": {
"debug": "^4.2.0",
"express": "^4.12.0",
"fp-ts": "^2.9.3",
"immer": "^8.0.0",
"io-ts": "^2.2.13",
"ioredis": "^4.17.3",
"next": "^9.5.3",
"nodemon": "^1.4.1",
"peer": "^0.5.3",
"peerjs": "^1.3.1",
"react": "^16.13.1",
"react-device-detect": "^1.15.0",
"react-dom": "^16.13.1",
"react-qr": "0.0.2",
"styled-components": "^5.2.0",
"twilio": "^2.9.1",
"use-http": "^1.0.16",
"webrtcsupport": "^2.2.0",
"xkcd-password": "^1.2.0"
},

@ -1,24 +0,0 @@
import React from 'react'
export default class DownloadButton extends React.Component {
constructor() {
super()
this.onClick = this.onClick.bind(this)
}
onClick(e) {
this.props.onClick(e)
}
render() {
return (
<button className="download-button" onClick={this.onClick}>
Download
</button>
)
}
}
DownloadButton.propTypes = {
onClick: React.PropTypes.func.isRequired,
}

@ -1,103 +0,0 @@
import React from 'react'
import DownloadActions from '../actions/DownloadActions'
import DownloadStore from '../stores/DownloadStore'
import { formatSize } from '../util'
import ChromeNotice from './ChromeNotice'
import DownloadButton from './DownloadButton'
import ErrorPage from './ErrorPage'
import ProgressBar from './ProgressBar'
import Spinner from './Spinner'
export default class DownloadPage extends React.Component {
constructor() {
super()
this.state = DownloadStore.getState()
this._onChange = () => {
this.setState(DownloadStore.getState())
}
this.downloadFile = this.downloadFile.bind(this)
}
componentDidMount() {
DownloadStore.listen(this._onChange)
}
componentWillUnmount() {
DownloadStore.unlisten(this._onChange)
}
downloadFile() {
DownloadActions.requestDownload()
}
render() {
switch (this.state.status) {
case 'ready':
return (
<div className="page">
<h1>FilePizza</h1>
<Spinner
dir="down"
name={this.state.fileName}
size={this.state.fileSize}
/>
<ChromeNotice />
<p className="notice">
Peers: {this.state.peers} &middot; Up:{' '}
{formatSize(this.state.speedUp)} &middot; Down:{' '}
{formatSize(this.state.speedDown)}
</p>
<DownloadButton onClick={this.downloadFile} />
</div>
)
case 'requesting':
case 'downloading':
return (
<div className="page">
<h1>FilePizza</h1>
<Spinner
dir="down"
animated
name={this.state.fileName}
size={this.state.fileSize}
/>
<ChromeNotice />
<p className="notice">
Peers: {this.state.peers} &middot; Up:{' '}
{formatSize(this.state.speedUp)} &middot; Down:{' '}
{formatSize(this.state.speedDown)}
</p>
<ProgressBar value={this.state.progress} />
</div>
)
case 'done':
return (
<div className="page">
<h1>FilePizza</h1>
<Spinner
dir="down"
name={this.state.fileName}
size={this.state.fileSize}
/>
<ChromeNotice />
<p className="notice">
Peers: {this.state.peers} &middot; Up:{' '}
{formatSize(this.state.speedUp)} &middot; Down:{' '}
{formatSize(this.state.speedDown)}
</p>
<ProgressBar value={1} />
</div>
)
default:
return <ErrorPage />
}
}
}

@ -1,56 +1,108 @@
import React from 'react'
import React, { useCallback, useEffect, useState } from 'react'
import { useWebRTC } from './WebRTCProvider'
import {
browserName,
browserVersion,
osName,
osVersion,
mobileVendor,
mobileModel,
} from 'react-device-detect'
import * as t from 'io-ts'
import { decodeMessage, Message, MessageType } from '../messages'
interface Props {
export default function Downloader({
uploaderPeerID,
}: {
uploaderPeerID: string
}
}): JSX.Element {
const peer = useWebRTC()
const Downloader: React.FC<Props> = ({ uploaderPeerID }: Props) => {
// const room = useRef<Room | null>(null)
// const peerData = usePeerData()
// useEffect(() => {
// if (room.current) return
// room.current = peerData.connect(roomName)
// console.log(room.current)
// setInterval(() => console.log(room.current), 1000)
// room.current
// .on('participant', (participant) => {
// console.log(participant.getId() + ' joined')
// participant.newDataChannel()
// participant
// .on('connected', () => {
// console.log('connected', participant.id)
// })
// .on('disconnected', () => {
// console.log('disconnected', participant.id)
// })
// .on('track', (event) => {
// console.log('stream', participant.id, event.streams[0])
// })
// .on('message', (payload) => {
// console.log(participant.id, payload)
// })
// .on('error', (event) => {
// console.error('peer', participant.id, event)
// participant.renegotiate()
// })
// console.log(participant)
// participant.send(`hello there, I'm the downloader`)
// })
// .on('error', (event) => {
// console.error('room', roomName, event)
// })
// return () => {
// room.current.disconnect()
// room.current = null
// }
// }, [peerData])
return null
}
const [password, setPassword] = useState('')
const [shouldAttemptConnection, setShouldAttemptConnection] = useState(false)
const [open, setOpen] = useState(false)
const [errorMessage, setErrorMessage] = useState<string | null>(null)
useEffect(() => {
if (!shouldAttemptConnection) {
return
}
const conn = peer.connect(uploaderPeerID, {
reliable: true,
})
conn.on('open', () => {
setOpen(true)
const request: t.TypeOf<typeof Message> = {
type: MessageType.Start,
browserName: browserName,
browserVersion: browserVersion,
osName: osName,
osVersion: osVersion,
mobileVendor: mobileVendor,
mobileModel: mobileModel,
password,
}
conn.send(request)
})
conn.on('data', (data) => {
try {
const message = decodeMessage(data)
switch (message.type) {
case MessageType.Info:
console.log(message)
break
export default Downloader
case MessageType.Error:
console.error(message.error)
setErrorMessage(message.error)
conn.close()
break
}
} catch (err) {
console.error(err)
}
})
conn.on('close', () => {
setOpen(false)
setShouldAttemptConnection(false)
})
return () => {
if (conn.open) conn.close()
}
}, [peer, password, shouldAttemptConnection])
const handleChangePassword = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setPassword(e.target.value)
},
[],
)
const handleSubmit = useCallback((ev) => {
ev.preventDefault()
setShouldAttemptConnection(true)
}, [])
if (open) {
return <div>Downloading</div>
}
if (shouldAttemptConnection) {
return <div>Loading...</div>
}
return (
<form action="#" method="post" onSubmit={handleSubmit}>
{errorMessage && <div style={{ color: 'red' }}>{errorMessage}</div>}
<input type="password" value={password} onChange={handleChangePassword} />
<button>Start</button>
</form>
)
}

@ -10,12 +10,13 @@ const Overlay = styled.div`
display: block;
`
interface Props {
onDrop: (files: any) => void
export default function DropZone({
children,
onDrop,
}: {
onDrop: (files: File[]) => void
children?: React.ReactNode
}
const Dropzone: React.FC<Props> = ({ children, onDrop }: Props) => {
}): JSX.Element {
const overlay = useRef()
const [focus, setFocus] = useState(false)
@ -62,5 +63,3 @@ const Dropzone: React.FC<Props> = ({ children, onDrop }: Props) => {
</Wrapper>
)
}
export default Dropzone

@ -1,104 +0,0 @@
import React from 'react'
import DropZone from './DropZone'
import Spinner from "./Spinner";
import Tempalink from "./Tempalink";
import UploadActions from "../actions/UploadActions";
import UploadStore from "../stores/UploadStore";
import socket from "filepizza-socket";
import { formatSize } from "../util";
export default class UploadPage extends React.Component {
constructor() {
super();
this.state = UploadStore.getState();
this._onChange = () => {
this.setState(UploadStore.getState());
};
this.uploadFile = this.uploadFile.bind(this);
}
componentDidMount() {
UploadStore.listen(this._onChange);
}
componentWillUnmount() {
UploadStore.unlisten(this._onChange);
}
uploadFile(file) {
UploadActions.uploadFile(file);
}
handleSelectedFile(event) {
const files = event.target.files;
if (files.length > 0) {
UploadActions.uploadFile(files[0]);
}
}
render() {
switch (this.state.status) {
case "ready":
return (
<DropZone onDrop={this.uploadFile}>
<div className="page">
<Spinner dir="up" />
<h1>FilePizza</h1>
<p>Free peer-to-peer file transfers in your browser.</p>
<small className="notice">
We never store anything. Files only served fresh.
</small>
<p>
<label className="select-file-label">
<input
type="file"
onChange={this.handleSelectedFile}
required
/>
<span>select a file</span>
</label>
</p>
</div>
</DropZone>
);
case "processing":
return (
<div className="page">
<Spinner dir="up" animated />
<h1>FilePizza</h1>
<p>Processing...</p>
</div>
);
case "uploading":
return (
<div className="page">
<h1>FilePizza</h1>
<Spinner
dir="up"
animated
name={this.state.fileName}
size={this.state.fileSize}
/>
<p>Send someone this link to download.</p>
<small className="notice">
This link will work as long as this page is open.
</small>
<p>
Peers: {this.state.peers} &middot; Up:{" "}
{formatSize(this.state.speedUp)}
</p>
<Tempalink
token={this.state.token}
shortToken={this.state.shortToken}
/>
</div>
);
}
}

@ -1,54 +1,244 @@
import React, { useRef, useEffect } from 'react'
import React, { useEffect, useState } from 'react'
import { UploadedFile } from '../types'
import { useWebRTC } from './WebRTCProvider'
import useFetch from 'use-http'
import Peer, { DataConnection } from 'peerjs'
import { decodeMessage, Message, MessageType } from '../messages'
import produce from 'immer'
import * as t from 'io-ts'
interface Props {
roomName: string
files: UploadedFile[]
enum UploaderConnectionStatus {
Pending = 'PENDING',
Uploading = 'UPLOADING',
Done = 'DONE',
InvalidPassword = 'INVALID_PASSWORD',
Closed = 'CLOSED',
Paused = 'PAUSED',
}
type UploaderConnection = {
status: UploaderConnectionStatus
dataConnection: DataConnection
browserName?: string
browserVersion?: string
osName?: string
osVersion?: string
mobileVendor?: string
mobileModel?: string
}
const RENEW_INTERVAL = 5000 // 20 minutes
function useUploaderChannel(
uploaderPeerID: string,
): {
loading: boolean
error: Error | null
longSlug: string
shortSlug: string
} {
const { loading, error, data } = useFetch(
'/api/create',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ uploaderPeerID }),
},
[uploaderPeerID],
)
if (!data) {
return { loading, error, longSlug: null, shortSlug: null }
}
return {
loading: false,
error: null,
longSlug: data.longSlug,
shortSlug: data.shortSlug,
}
}
function useUploaderChannelRenewal(shortSlug: string): void {
const { post } = useFetch('/api/renew')
useEffect(() => {
let timeout = null
const run = (): void => {
timeout = setTimeout(() => {
post({ slug: shortSlug })
.then(() => {
run()
})
.catch((err) => {
console.error(err)
run()
})
}, RENEW_INTERVAL)
}
run()
return () => {
clearTimeout(timeout)
}
}, [shortSlug])
}
const Uploader: React.FC<Props> = ({ roomName, files }: Props) => {
const room = useRef<Room | null>(null)
function useUploaderConnections(
peer: Peer,
files: UploadedFile[],
password: string,
): Array<UploaderConnection> {
const [connections, setConnections] = useState<Array<UploaderConnection>>([])
useEffect(() => {
peer.on('connection', (conn: DataConnection) => {
const newConn = {
status: UploaderConnectionStatus.Pending,
dataConnection: conn,
}
setConnections((conns) => [...conns, newConn])
const updateConnection = (
fn: (draftConn: UploaderConnection) => void,
) => {
setConnections((conns) =>
produce(conns, (draft) => {
const updatedConn = draft.find((c) => c.dataConnection === conn)
if (!updatedConn) {
return
}
fn(updatedConn)
}),
)
}
conn.on('data', (data): void => {
try {
const message = decodeMessage(data)
switch (message.type) {
case MessageType.RequestInfo: {
if (message.password !== password) {
const request: t.TypeOf<typeof Message> = {
type: MessageType.Error,
error: 'Invalid password',
}
conn.send(request)
updateConnection((draft) => {
if (draft.status !== UploaderConnectionStatus.Pending) {
return
}
draft.status = UploaderConnectionStatus.InvalidPassword
draft.browserName = message.browserName
draft.browserVersion = message.browserVersion
draft.osName = message.osName
draft.osVersion = message.osVersion
draft.mobileVendor = message.mobileVendor
draft.mobileModel = message.mobileModel
})
return
}
updateConnection((draft) => {
if (draft.status !== UploaderConnectionStatus.Pending) {
return
}
draft.status = UploaderConnectionStatus.Uploading
draft.browserName = message.browserName
draft.browserVersion = message.browserVersion
draft.osName = message.osName
draft.osVersion = message.osVersion
draft.mobileVendor = message.mobileVendor
draft.mobileModel = message.mobileModel
})
const fileInfo = files.map((f) => {
return {
fullPath: f.fullPath,
}
})
const request: t.TypeOf<typeof Message> = {
type: MessageType.Info,
files: fileInfo,
}
conn.send(request)
// TODO(@kern): Handle sending chunks
break
}
}
} catch (err) {
console.error(err)
}
})
conn.on('close', (): void => {
updateConnection((draft) => {
if (draft.status === UploaderConnectionStatus.InvalidPassword) {
return
}
draft.status = UploaderConnectionStatus.Closed
})
})
})
}, [peer, files, password])
return connections
}
export default function Uploader({
files,
password,
}: {
files: UploadedFile[]
password: string
}): JSX.Element {
const peer = useWebRTC()
const { longSlug, shortSlug } = useUploaderChannel(peer.id)
useUploaderChannelRenewal(shortSlug)
const connections = useUploaderConnections(peer, files, password)
// useEffect(() => {
// room.current = peerData.connect(roomName)
// room.current
// .on('participant', (participant) => {
// console.log(participant.getId() + ' joined')
// participant.newDataChannel()
//
// participant
// .on('connected', () => {
// console.log('connected', participant.id)
// })
// .on('disconnected', () => {
// console.log('disconnected', participant.id)
// })
// .on('track', (event) => {
// console.log('stream', participant.id, event.streams[0])
// })
// .on('message', (payload) => {
// console.log(participant.id, payload)
// })
// .on('error', (event) => {
// console.error('peer', participant.id, event)
// participant.renegotiate()
// })
// participant.send(`hello there, I'm the uploader`)
// })
// .on('error', (event) => {
// console.error('room', roomName, event)
// })
//
// return () => {
// room.current.disconnect()
// room.current = null
// }
// }, [peerData])
if (!longSlug || !shortSlug) {
return null
}
const longURL = `/download/${longSlug}`
const shortURL = `/download/${shortSlug}`
const items = files.map((f) => <li key={f.fullPath}>{f.fullPath}</li>)
return <ul>{items}</ul>
return (
<>
<div>
Long:
<a href={longURL} target="_blank">
{longURL}
</a>
</div>
<div>
Short:
<a href={shortURL} target="_blank">
{shortURL}
</a>
</div>
<ul>{items}</ul>
<h2>Connections</h2>
<ul>
{connections.map((conn) => (
<li>
{conn.status} {conn.browserName} {conn.browserVersion}
</li>
))}
</ul>
</>
)
}
export default Uploader

@ -14,36 +14,38 @@ const ICE_SERVERS: RTCConfiguration = {
],
}
interface Props {
servers?: RTCConfiguration
children?: React.ReactNode
}
const WebRTCContext = React.createContext<WebRTCValue>(null)
const WebRTCContext = React.createContext<WebRTCValue | null>(null)
export const useWebRTC = (): WebRTCValue => {
return useContext(WebRTCContext)
return useContext(WebRTCContext)!
}
export const WebRTCProvider: React.FC<Props> = ({
export function WebRTCProvider({
servers = ICE_SERVERS,
children,
}: Props) => {
const [pageLoaded, setPageLoaded] = useState(false)
const peer = useRef<WebRTCValue>(null)
}: {
servers?: RTCConfiguration
children?: React.ReactNode
}): JSX.Element {
const [loaded, setLoaded] = useState(false)
const peer = useRef<WebRTCValue | null>(null)
useEffect(() => {
const effect = async () => {
peer.current = new Peer(undefined, {
const peerObj = new Peer(undefined, {
config: servers,
})
setPageLoaded(true)
peerObj.on('open', () => {
peer.current = peerObj
setLoaded(true)
})
}
effect()
}, [])
if (!pageLoaded || !peer.current) {
if (!loaded || !peer.current) {
return null
}

@ -58,3 +58,15 @@ export const extractFileList = async (e: React.DragEvent): Promise<File[]> => {
return [scanResults, fileResults].flat(2)
}
// Borrowed from StackOverflow
// http://stackoverflow.com/questions/15900485/correct-way-to-convert-size-in-bytes-to-kb-mb-gb-in-javascript
export const formatSize = (bytes: number): string => {
if (bytes === 0) {
return '0 Bytes'
}
const k = 1000
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${(bytes / Math.pow(k, i)).toPrecision(3)} ${sizes[i]}`
}

@ -1,52 +0,0 @@
const twilio = require('twilio')
const winston = require('winston')
if (process.env.TWILIO_SID && process.env.TWILIO_TOKEN) {
const twilioSID = process.env.TWILIO_SID
const twilioToken = process.env.TWILIO_TOKEN
var client = twilio(twilioSID, twilioToken)
winston.info('Using Twilio TURN service')
} else {
var client = null
}
let ICE_SERVERS = [
{
urls: 'stun:stun.l.google.com:19302',
},
]
if (process.env.ICE_SERVERS) {
ICE_SERVERS = JSON.parse(process.env.ICE_SERVERS)
}
const CACHE_LIFETIME = 5 * 60 * 1000 // 5 minutes
let cachedPromise = null
function clearCache() {
cachedPromise = null
}
exports.getICEServers = function() {
if (client == null) {
return Promise.resolve(ICE_SERVERS)
}
if (cachedPromise) {
return cachedPromise
}
cachedPromise = new Promise((resolve, reject) => {
client.tokens.create({}, (err, token) => {
if (err) {
winston.error(err)
return resolve(DEFAULT_ICE_SERVERS)
}
winston.info('Retrieved ICE servers from Twilio')
setTimeout(clearCache, CACHE_LIFETIME)
resolve(token.ice_servers)
})
})
return cachedPromise
}

@ -0,0 +1,77 @@
import * as t from 'io-ts'
import { pipe } from 'fp-ts/function'
import { fold } from 'fp-ts/Either'
export enum MessageType {
RequestInfo = 'REQUEST_INFO',
Info = 'INFO',
Start = 'START',
Chunk = 'CHUNK',
Pause = 'PAUSE',
Error = 'ERROR',
}
const RequestInfoMessage = t.type({
type: t.literal(MessageType.RequestInfo),
browserName: t.string,
browserVersion: t.string,
osName: t.string,
osVersion: t.string,
mobileVendor: t.string,
mobileModel: t.string,
password: t.string,
})
const InfoMessage = t.type({
type: t.literal(MessageType.Info),
files: t.array(
t.type({
fullPath: t.string,
}),
),
})
const StartMessage = t.type({
type: t.literal(MessageType.Start),
browserName: t.string,
browserVersion: t.string,
osName: t.string,
osVersion: t.string,
mobileVendor: t.string,
mobileModel: t.string,
password: t.string,
})
const ChunkMessage = t.type({
type: t.literal(MessageType.Chunk),
// TODO(@kern): Chunk
})
const PauseMessage = t.type({
type: t.literal(MessageType.Pause),
// TODO(@kern): Pausing
})
const ErrorMessage = t.type({
type: t.literal(MessageType.Error),
error: t.string,
})
export const Message = t.union([
RequestInfoMessage,
InfoMessage,
StartMessage,
ChunkMessage,
PauseMessage,
ErrorMessage,
])
export function decodeMessage(data: any): t.TypeOf<typeof Message> {
const onFailure = (errors: t.Errors): t.TypeOf<typeof Message> => {
throw new Error(`${errors.length} error(s) found`)
}
const onSuccess = (mesg: t.TypeOf<typeof Message>) => mesg
return pipe(Message.decode(data), fold(onFailure, onSuccess))
}

@ -2,9 +2,10 @@ import { NextApiRequest, NextApiResponse } from 'next'
import { channelRepo } from '../../channel'
import { routeHandler, getBodyKey } from '../../routes'
export default routeHandler<void>(
(req: NextApiRequest, _res: NextApiResponse): Promise<void> => {
export default routeHandler<boolean>(
async (req: NextApiRequest, _res: NextApiResponse): Promise<boolean> => {
const slug = getBodyKey(req, 'slug')
return channelRepo.renew(slug)
await channelRepo.renew(slug)
return true
},
)

@ -10,14 +10,10 @@ type Props = {
error?: string
}
const DownloadPage: NextPage<Props> = ({ slug, uploaderPeerID }) => {
const DownloadPage: NextPage<Props> = ({ uploaderPeerID }) => {
return (
<WebRTCProvider>
<>
<div>{slug}</div>
<div>{uploaderPeerID}</div>
<Downloader uploaderPeerID={uploaderPeerID} />
</>
<Downloader uploaderPeerID={uploaderPeerID} />
</WebRTCProvider>
)
}

@ -1,6 +1,6 @@
import React, { useCallback, useState } from 'react'
import WebRTCProvider from '../components/WebRTCProvider'
import Dropzone from '../components/Dropzone'
import DropZone from '../components/DropZone'
import UploadFileList from '../components/UploadFileList'
import Uploader from '../components/Uploader'
import PasswordField from '../components/PasswordField'
@ -15,7 +15,6 @@ export const IndexPage: NextPage = () => {
const [uploading, setUploading] = useState(false)
const handleDrop = useCallback((files: UploadedFile[]): void => {
console.log('Received files', files)
setUploadedFiles(files)
}, [])
@ -34,7 +33,7 @@ export const IndexPage: NextPage = () => {
if (!uploadedFiles.length) {
return (
<>
<Dropzone onDrop={handleDrop}>Drop a file to get started.</Dropzone>
<DropZone onDrop={handleDrop}>Drop a file to get started.</DropZone>
</>
)
}
@ -51,11 +50,9 @@ export const IndexPage: NextPage = () => {
return (
<WebRTCProvider>
<>
<UploadFileList files={uploadedFiles} />
<StopButton onClick={handleStop} />
<Uploader roomName={'my-room'} files={uploadedFiles} />
</>
<UploadFileList files={uploadedFiles} />
<StopButton onClick={handleStop} />
<Uploader files={uploadedFiles} password={password} />
</WebRTCProvider>
)
}

@ -15,10 +15,11 @@ export const generateShortSlug = (): string => {
const longSlugGenerator = new xkcdPassword()
longSlugGenerator.initWithWordList(config.longSlug.words)
export const generateLongSlug = (): Promise<string> => {
return longSlugGenerator.generate({
export const generateLongSlug = async (): Promise<string> => {
const parts = await longSlugGenerator.generate({
numWords: config.longSlug.numWords,
minLength: 1,
maxLength: 256,
})
return parts.join('/')
}

@ -1,11 +0,0 @@
// Borrowed from StackOverflow
// http://stackoverflow.com/questions/15900485/correct-way-to-convert-size-in-bytes-to-kb-mb-gb-in-javascript
export const formatSize = (bytes: number): string => {
if (bytes === 0) {
return '0 Bytes'
}
const k = 1000
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${(bytes / Math.pow(k, i)).toPrecision(3)} ${sizes[i]}`
}

@ -3749,6 +3749,11 @@ forwarded@~0.1.2:
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=
fp-ts@^2.9.3:
version "2.9.3"
resolved "https://registry.yarnpkg.com/fp-ts/-/fp-ts-2.9.3.tgz#419b9103018ddf7c785acf2b3fecb2662afab5af"
integrity sha512-NjzcHYgigcbPQ6yJ52zwgsVDwKz3vwy9sjbxyzcvfXQm+j1BGeOPRuzLKEwsLyE4Xut6gG1FXJtsU9/gUB7tXg==
fragment-cache@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19"
@ -4201,6 +4206,11 @@ ignore@^5.1.4:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57"
integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==
immer@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/immer/-/immer-8.0.0.tgz#08763549ba9dd7d5e2eb4bec504a8315bd9440c2"
integrity sha512-jm87NNBAIG4fHwouilCHIecFXp5rMGkiFrAuhVO685UnMAlOneEAnOyzPt8OnP47TC11q/E7vpzZe0WvwepFTg==
import-fresh@^3.0.0, import-fresh@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66"
@ -4278,6 +4288,11 @@ invariant@^2.2.2, invariant@^2.2.4:
dependencies:
loose-envify "^1.0.0"
io-ts@^2.2.13:
version "2.2.13"
resolved "https://registry.yarnpkg.com/io-ts/-/io-ts-2.2.13.tgz#e04016685e863dd5cffb2b5262c5c9c74432dfda"
integrity sha512-BYJgE/BanovJKDvCnAkrr7f3gTucSyk+Sr5VtpouBO1/YfBKUyIn2z1ODG8LEF+1D4sjKZ3Bd/A5/v8JrJe5UQ==
ioredis@^4.17.3:
version "4.17.3"
resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.17.3.tgz#9938c60e4ca685f75326337177bdc2e73ae9c9dc"
@ -6171,6 +6186,13 @@ rc@^1.0.1, rc@^1.1.6:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
react-device-detect@^1.15.0:
version "1.15.0"
resolved "https://registry.yarnpkg.com/react-device-detect/-/react-device-detect-1.15.0.tgz#5321f94ae3c4d51ef399b0502a6c739e32d0f315"
integrity sha512-ywjtWW04U7vaJK87IAFHhKozZhTPeDVWsfYx5CxQSQCjU5+fnMMxWZt9HnVWaNTqBEn6g8wCNWyqav7sXJrURg==
dependencies:
ua-parser-js "^0.7.23"
react-dom@^16.13.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.13.1.tgz#c1bd37331a0486c078ee54c4740720993b2e0e7f"
@ -7529,6 +7551,11 @@ typescript@^4.0.3:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.3.tgz#153bbd468ef07725c1df9c77e8b453f8d36abba5"
integrity sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg==
ua-parser-js@^0.7.23:
version "0.7.23"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.23.tgz#704d67f951e13195fbcd3d78818577f5bc1d547b"
integrity sha512-m4hvMLxgGHXG3O3fQVAyyAQpZzDOvwnhOTjYz5Xmr7r/+LpkNy3vJXdVRWgd1TkAb7NGROZuSy96CrlNVjA7KA==
undefsafe@^2.0.2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.3.tgz#6b166e7094ad46313b2202da7ecc2cd7cc6e7aae"
@ -7666,6 +7693,25 @@ url@^0.11.0:
punycode "1.3.2"
querystring "0.2.0"
urs@^0.0.7:
version "0.0.7"
resolved "https://registry.yarnpkg.com/urs/-/urs-0.0.7.tgz#2dbf320a68a54bbd67ab9c5ac103b0f2227ca6be"
integrity sha512-NDwuby5BP7D60RjHlow8Gch4yfglLdCpZagGeVDZNcL+jsA2v6RcBhPTJraqq1oQ1U3LT1bQfgmwgNUUIFht+g==
use-http@^1.0.16:
version "1.0.16"
resolved "https://registry.yarnpkg.com/use-http/-/use-http-1.0.16.tgz#6d75f3e734abf990049c7a02b22d757c3abda883"
integrity sha512-t2/Q6Ic644Jn9z62sOVhfURXX58429NwgRDvJZikxhDjLG5FrZ0SLLxGgm4GYznOnKBqkXKVrhW/wdjwXkw1rA==
dependencies:
urs "^0.0.7"
use-ssr "^1.0.22"
utility-types "^3.10.0"
use-ssr@^1.0.22:
version "1.0.23"
resolved "https://registry.yarnpkg.com/use-ssr/-/use-ssr-1.0.23.tgz#3bde1e10cd01b3b61ab6386d7cddb72e74828bf8"
integrity sha512-5bvlssgROgPgIrnILJe2mJch4e2Id0/bVm1SQzqvPvEAXmlsinCCVHWK3a2iHcPat7PkdJHBo0gmSmODIz6tNA==
use-subscription@1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/use-subscription/-/use-subscription-1.4.1.tgz#edcbcc220f1adb2dd4fa0b2f61b6cc308e620069"
@ -7697,6 +7743,11 @@ util@^0.11.0:
dependencies:
inherits "2.0.3"
utility-types@^3.10.0:
version "3.10.0"
resolved "https://registry.yarnpkg.com/utility-types/-/utility-types-3.10.0.tgz#ea4148f9a741015f05ed74fd615e1d20e6bed82b"
integrity sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==
utils-merge@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"

Loading…
Cancel
Save