download working
This commit is contained in:
@@ -39,7 +39,7 @@ function AppContent() {
|
|||||||
const settings = useSelector((state: RootState) => state.settings)
|
const settings = useSelector((state: RootState) => state.settings)
|
||||||
const status = useSelector((state: RootState) => state.status)
|
const status = useSelector((state: RootState) => state.status)
|
||||||
|
|
||||||
const socket = useMemo(() => io(getWebSocketEndpoint()), [])
|
const socket = useMemo(() => new WebSocket(getWebSocketEndpoint()), [])
|
||||||
|
|
||||||
const mode = settings.theme
|
const mode = settings.theme
|
||||||
|
|
||||||
@@ -60,9 +60,7 @@ function AppContent() {
|
|||||||
|
|
||||||
/* Get disk free space */
|
/* Get disk free space */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
socket.on('free-space', (res: string) => {
|
|
||||||
setFreeDiskSpace(res)
|
|
||||||
})
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -21,18 +21,19 @@ import {
|
|||||||
import { Buffer } from 'buffer';
|
import { Buffer } from 'buffer';
|
||||||
import { Fragment, useEffect, useMemo, useState } from "react";
|
import { Fragment, useEffect, useMemo, useState } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { Socket } from "socket.io-client";
|
|
||||||
import { CliArguments } from "./classes";
|
import { CliArguments } from "./classes";
|
||||||
import { StackableResult } from "./components/StackableResult";
|
import { StackableResult } from "./components/StackableResult";
|
||||||
import { serverStates } from "./events";
|
import { serverStates } from "./events";
|
||||||
import { connected, downloading, finished } from "./features/status/statusSlice";
|
import { connected } from "./features/status/statusSlice";
|
||||||
import { I18nBuilder } from "./i18n";
|
import { I18nBuilder } from "./i18n";
|
||||||
import { IDLMetadata, IDLMetadataAndPID, IMessage } from "./interfaces";
|
import { IDLMetadata, IMessage } from "./interfaces";
|
||||||
|
import { RPCClient } from "./rpcClient";
|
||||||
import { RootState } from "./stores/store";
|
import { RootState } from "./stores/store";
|
||||||
|
import { RPCResult } from "./types";
|
||||||
import { isValidURL, toFormatArgs, updateInStateMap } from "./utils";
|
import { isValidURL, toFormatArgs, updateInStateMap } from "./utils";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
socket: Socket
|
socket: WebSocket
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Home({ socket }: Props) {
|
export default function Home({ socket }: Props) {
|
||||||
@@ -44,6 +45,8 @@ export default function Home({ socket }: Props) {
|
|||||||
// ephemeral state
|
// ephemeral state
|
||||||
const [progressMap, setProgressMap] = useState(new Map<number, number>());
|
const [progressMap, setProgressMap] = useState(new Map<number, number>());
|
||||||
const [messageMap, setMessageMap] = useState(new Map<number, IMessage>());
|
const [messageMap, setMessageMap] = useState(new Map<number, IMessage>());
|
||||||
|
|
||||||
|
const [activeDownloads, setActiveDownloads] = useState(new Array<RPCResult>());
|
||||||
const [downloadInfoMap, setDownloadInfoMap] = useState(new Map<number, IDLMetadata>());
|
const [downloadInfoMap, setDownloadInfoMap] = useState(new Map<number, IDLMetadata>());
|
||||||
const [downloadFormats, setDownloadFormats] = useState<IDLMetadata>();
|
const [downloadFormats, setDownloadFormats] = useState<IDLMetadata>();
|
||||||
const [pickedVideoFormat, setPickedVideoFormat] = useState('');
|
const [pickedVideoFormat, setPickedVideoFormat] = useState('');
|
||||||
@@ -62,63 +65,44 @@ export default function Home({ socket }: Props) {
|
|||||||
|
|
||||||
// memos
|
// memos
|
||||||
const i18n = useMemo(() => new I18nBuilder(settings.language), [settings.language])
|
const i18n = useMemo(() => new I18nBuilder(settings.language), [settings.language])
|
||||||
|
const client = useMemo(() => new RPCClient(socket), [settings.serverAddr, settings.serverPort])
|
||||||
const cliArgs = useMemo(() => new CliArguments().fromString(settings.cliArgs), [settings.cliArgs])
|
const cliArgs = useMemo(() => new CliArguments().fromString(settings.cliArgs), [settings.cliArgs])
|
||||||
|
|
||||||
/* -------------------- Effects -------------------- */
|
/* -------------------- Effects -------------------- */
|
||||||
/* WebSocket connect event handler*/
|
/* WebSocket connect event handler*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
socket.on('connect', () => {
|
socket.onopen = () => {
|
||||||
dispatch(connected())
|
dispatch(connected())
|
||||||
socket.emit('fetch-jobs')
|
console.log('oke')
|
||||||
socket.emit('disk-space')
|
socket.send('fetch-jobs')
|
||||||
socket.emit('retrieve-jobs')
|
socket.send('disk-space')
|
||||||
});
|
socket.send('retrieve-jobs')
|
||||||
}, [])
|
}
|
||||||
|
}, [])
|
||||||
/* Ask server for pending jobs / background jobs */
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
socket.on('pending-jobs', (count: number) => {
|
const interval = setInterval(() => client.running(), 1000)
|
||||||
count === 0 ? setShowBackdrop(false) : setShowBackdrop(true)
|
return () => clearInterval(interval)
|
||||||
})
|
}, [])
|
||||||
}, [])
|
|
||||||
|
useEffect(() => {
|
||||||
/* Handle download information sent by server */
|
socket.onmessage = (event) => {
|
||||||
useEffect(() => {
|
const res = client.decode(event.data)
|
||||||
socket.on('available-formats', (data: IDLMetadata) => {
|
if (showBackdrop) {
|
||||||
setShowBackdrop(false)
|
setShowBackdrop(false)
|
||||||
setDownloadFormats(data);
|
}
|
||||||
})
|
switch (typeof res.result) {
|
||||||
}, [])
|
case 'object':
|
||||||
|
setActiveDownloads(
|
||||||
/* Handle download information sent by server */
|
res.result
|
||||||
useEffect(() => {
|
.filter((r: RPCResult) => !!r.info.url)
|
||||||
socket.on('metadata', (data: IDLMetadataAndPID) => {
|
.sort((a: RPCResult, b: RPCResult) => a.info.title.localeCompare(b.info.title))
|
||||||
setShowBackdrop(false)
|
)
|
||||||
dispatch(downloading())
|
break
|
||||||
updateInStateMap<number, IDLMetadata>(data.pid, data.metadata, downloadInfoMap, setDownloadInfoMap);
|
default:
|
||||||
})
|
break
|
||||||
}, [])
|
|
||||||
|
|
||||||
/* Handle per-download progress */
|
|
||||||
useEffect(() => {
|
|
||||||
socket.on('progress', (data: IMessage) => {
|
|
||||||
if (data.status === serverStates.PROG_DONE || data.status === serverStates.PROC_ABORT) {
|
|
||||||
setShowBackdrop(false)
|
|
||||||
updateInStateMap<number, IMessage>(data.pid, serverStates.PROG_DONE, messageMap, setMessageMap);
|
|
||||||
updateInStateMap<number, number>(data.pid, 0, progressMap, setProgressMap);
|
|
||||||
socket.emit('disk-space')
|
|
||||||
dispatch(finished())
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
updateInStateMap<number, IMessage>(data.pid, data, messageMap, setMessageMap);
|
|
||||||
if (data.progress) {
|
|
||||||
updateInStateMap<number, number>(data.pid,
|
|
||||||
Math.ceil(Number(data.progress.replace('%', ''))),
|
|
||||||
progressMap,
|
|
||||||
setProgressMap
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -140,12 +124,10 @@ export default function Home({ socket }: Props) {
|
|||||||
if (pickedAudioFormat !== '') codes.push(pickedAudioFormat);
|
if (pickedAudioFormat !== '') codes.push(pickedAudioFormat);
|
||||||
if (pickedBestFormat !== '') codes.push(pickedBestFormat);
|
if (pickedBestFormat !== '') codes.push(pickedBestFormat);
|
||||||
|
|
||||||
socket.emit('send-url', {
|
client.download(
|
||||||
url: immediate || url || workingUrl,
|
immediate || url || workingUrl,
|
||||||
path: availableDownloadPaths[downloadPath],
|
cliArgs.toString() + toFormatArgs(codes),
|
||||||
params: cliArgs.toString() + toFormatArgs(codes),
|
)
|
||||||
renameTo: fileNameOverride,
|
|
||||||
})
|
|
||||||
|
|
||||||
setUrl('')
|
setUrl('')
|
||||||
setWorkingUrl('')
|
setWorkingUrl('')
|
||||||
@@ -162,10 +144,6 @@ export default function Home({ socket }: Props) {
|
|||||||
* Retrive url from input and display the formats selection view
|
* Retrive url from input and display the formats selection view
|
||||||
*/
|
*/
|
||||||
const sendUrlFormatSelection = () => {
|
const sendUrlFormatSelection = () => {
|
||||||
socket.emit('send-url-format-selection', {
|
|
||||||
url: url,
|
|
||||||
})
|
|
||||||
|
|
||||||
setWorkingUrl(url)
|
setWorkingUrl(url)
|
||||||
setUrl('')
|
setUrl('')
|
||||||
setPickedAudioFormat('')
|
setPickedAudioFormat('')
|
||||||
@@ -199,14 +177,12 @@ export default function Home({ socket }: Props) {
|
|||||||
* @param id The download id / pid
|
* @param id The download id / pid
|
||||||
* @returns void
|
* @returns void
|
||||||
*/
|
*/
|
||||||
const abort = (id?: number) => {
|
const abort = (id?: string) => {
|
||||||
if (id) {
|
if (id) {
|
||||||
updateInStateMap(id, null, downloadInfoMap, setDownloadInfoMap, true)
|
client.kill(id)
|
||||||
socket.emit('abort', { pid: id })
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setDownloadFormats(undefined)
|
client.killAll()
|
||||||
socket.emit('abort-all')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseUrlListFile = (event: any) => {
|
const parseUrlListFile = (event: any) => {
|
||||||
@@ -451,30 +427,17 @@ export default function Home({ socket }: Props) {
|
|||||||
}
|
}
|
||||||
<Grid container spacing={{ xs: 2, md: 2 }} columns={{ xs: 4, sm: 8, md: 12 }} pt={2}>
|
<Grid container spacing={{ xs: 2, md: 2 }} columns={{ xs: 4, sm: 8, md: 12 }} pt={2}>
|
||||||
{
|
{
|
||||||
Array
|
activeDownloads.map(download => (
|
||||||
.from<any>(messageMap)
|
<Grid item xs={4} sm={8} md={6} key={download.id}>
|
||||||
.filter(flattened => [...flattened][0])
|
|
||||||
.filter(flattened => [...flattened][1].toString() !== serverStates.PROG_DONE)
|
|
||||||
.flatMap(message => (
|
|
||||||
<Grid item xs={4} sm={8} md={6} key={message[0]}>
|
|
||||||
{
|
|
||||||
/*
|
|
||||||
Message[0] => key, the pid which is shared with the progress and download Maps
|
|
||||||
Message[1] => value, the actual formatted message sent from server
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<StackableResult
|
<StackableResult
|
||||||
formattedLog={message[1]}
|
title={download.info.title}
|
||||||
title={downloadInfoMap.get(message[0])?.title ?? ''}
|
thumbnail={download.info.thumbnail}
|
||||||
thumbnail={downloadInfoMap.get(message[0])?.thumbnail ?? ''}
|
percentage={download.progress.percentage}
|
||||||
progress={progressMap.get(message[0]) ?? 0}
|
stopCallback={() => abort(download.id)}
|
||||||
stopCallback={() => abort(message[0])}
|
resolution={download.info.resolution ?? ''}
|
||||||
resolution={
|
speed={download.progress.speed}
|
||||||
settings.formatSelection
|
size={download.info.filesize_approx ?? 0}
|
||||||
? ''
|
|
||||||
: downloadInfoMap.get(message[0])?.best.resolution ?? ''
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ import { RootState } from "./stores/store";
|
|||||||
import { validateDomain, validateIP } from "./utils";
|
import { validateDomain, validateIP } from "./utils";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
socket: Socket
|
socket: WebSocket
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Settings({ socket }: Props) {
|
export default function Settings({ socket }: Props) {
|
||||||
@@ -112,8 +112,7 @@ export default function Settings({ socket }: Props) {
|
|||||||
* Send via WebSocket a message in order to update the yt-dlp binary from server
|
* Send via WebSocket a message in order to update the yt-dlp binary from server
|
||||||
*/
|
*/
|
||||||
const updateBinary = () => {
|
const updateBinary = () => {
|
||||||
socket.emit('update-bin')
|
|
||||||
dispatch(alreadyUpdated())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -4,15 +4,24 @@ import { IMessage } from "../interfaces";
|
|||||||
import { ellipsis } from "../utils";
|
import { ellipsis } from "../utils";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
formattedLog: IMessage,
|
|
||||||
title: string,
|
title: string,
|
||||||
thumbnail: string,
|
thumbnail: string,
|
||||||
resolution: string
|
resolution: string
|
||||||
progress: number,
|
percentage: string,
|
||||||
|
size: number,
|
||||||
|
speed: number,
|
||||||
stopCallback: VoidFunction,
|
stopCallback: VoidFunction,
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StackableResult({ formattedLog, title, thumbnail, resolution, progress, stopCallback }: Props) {
|
export function StackableResult({
|
||||||
|
title,
|
||||||
|
thumbnail,
|
||||||
|
resolution,
|
||||||
|
percentage,
|
||||||
|
speed,
|
||||||
|
size,
|
||||||
|
stopCallback
|
||||||
|
}: Props) {
|
||||||
const guessResolution = (xByY: string): any => {
|
const guessResolution = (xByY: string): any => {
|
||||||
if (!xByY) return null;
|
if (!xByY) return null;
|
||||||
if (xByY.includes('4320')) return (<EightK color="primary" />);
|
if (xByY.includes('4320')) return (<EightK color="primary" />);
|
||||||
@@ -22,6 +31,8 @@ export function StackableResult({ formattedLog, title, thumbnail, resolution, pr
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const percentageToNumber = () => Number(percentage.replace('%', ''))
|
||||||
|
|
||||||
const roundMB = (bytes: number) => `${(bytes / 1_000_000).toFixed(2)}MiB`
|
const roundMB = (bytes: number) => `${(bytes / 1_000_000).toFixed(2)}MiB`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -43,14 +54,14 @@ export function StackableResult({ formattedLog, title, thumbnail, resolution, pr
|
|||||||
<Skeleton />
|
<Skeleton />
|
||||||
}
|
}
|
||||||
<Stack direction="row" spacing={1} py={2}>
|
<Stack direction="row" spacing={1} py={2}>
|
||||||
<Chip label={formattedLog.status} color="primary" />
|
<Chip label={'Downloading'} color="primary" />
|
||||||
<Typography>{formattedLog.progress}</Typography>
|
<Typography>{percentage}</Typography>
|
||||||
<Typography>{formattedLog.dlSpeed}</Typography>
|
<Typography>{speed}</Typography>
|
||||||
<Typography>{roundMB(formattedLog.size ?? 0)}</Typography>
|
<Typography>{roundMB(size ?? 0)}</Typography>
|
||||||
{guessResolution(resolution)}
|
{guessResolution(resolution)}
|
||||||
</Stack>
|
</Stack>
|
||||||
{progress ?
|
{percentage ?
|
||||||
<LinearProgress variant="determinate" value={progress} /> :
|
<LinearProgress variant="determinate" value={percentageToNumber()} /> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
81
frontend/src/rpcClient.ts
Normal file
81
frontend/src/rpcClient.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import type { RPCRequest, RPCResponse } from "./types"
|
||||||
|
import type { IDLMetadata } from './interfaces'
|
||||||
|
|
||||||
|
export class RPCClient {
|
||||||
|
private socket: WebSocket
|
||||||
|
private seq: number
|
||||||
|
|
||||||
|
constructor(socket: WebSocket) {
|
||||||
|
this.socket = socket
|
||||||
|
this.seq = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private incrementSeq() {
|
||||||
|
return String(this.seq++)
|
||||||
|
}
|
||||||
|
|
||||||
|
private send(req: RPCRequest) {
|
||||||
|
this.socket.send(JSON.stringify(req))
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendHTTP<T>(req: RPCRequest) {
|
||||||
|
return new Promise<RPCResponse<T>>((resolve, reject) => {
|
||||||
|
fetch('/rpc-http', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(req)
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => resolve(data))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public download(url: string, args: string) {
|
||||||
|
if (url) {
|
||||||
|
this.send({
|
||||||
|
id: this.incrementSeq(),
|
||||||
|
method: 'Service.Exec',
|
||||||
|
params: [{
|
||||||
|
URL: url.split("?list").at(0)!,
|
||||||
|
Params: args.split(" ").map(a => a.trim()),
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public formats(url: string) {
|
||||||
|
if (url) {
|
||||||
|
return this.sendHTTP<IDLMetadata>({
|
||||||
|
id: this.incrementSeq(),
|
||||||
|
method: 'Service.Formats',
|
||||||
|
params: [{
|
||||||
|
URL: url.split("?list").at(0)!,
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public running() {
|
||||||
|
this.send({
|
||||||
|
method: 'Service.Running',
|
||||||
|
params: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public kill(id: string) {
|
||||||
|
this.send({
|
||||||
|
method: 'Service.Kill',
|
||||||
|
params: [id],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public killAll() {
|
||||||
|
this.send({
|
||||||
|
method: 'Service.KillAll',
|
||||||
|
params: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public decode(data: any): RPCResponse<any> {
|
||||||
|
return JSON.parse(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
43
frontend/src/types.d.ts
vendored
Normal file
43
frontend/src/types.d.ts
vendored
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
export type RPCMethods =
|
||||||
|
| "Service.Exec"
|
||||||
|
| "Service.Kill"
|
||||||
|
| "Service.Running"
|
||||||
|
| "Service.KillAll"
|
||||||
|
| "Service.FreeSpace"
|
||||||
|
| "Service.Formats"
|
||||||
|
|
||||||
|
export type RPCRequest = {
|
||||||
|
method: RPCMethods,
|
||||||
|
params?: any[],
|
||||||
|
id?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RPCResponse<T> = {
|
||||||
|
result: T,
|
||||||
|
error: number | null
|
||||||
|
id?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RPCResult = {
|
||||||
|
id: string
|
||||||
|
progress: {
|
||||||
|
speed: number
|
||||||
|
eta: number
|
||||||
|
percentage: string
|
||||||
|
}
|
||||||
|
info: {
|
||||||
|
url: string
|
||||||
|
filesize_approx?: number
|
||||||
|
resolution?: string
|
||||||
|
thumbnail: string
|
||||||
|
title: string
|
||||||
|
vcodec?: string
|
||||||
|
acodec?: string
|
||||||
|
ext?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RPCParams = {
|
||||||
|
URL: string
|
||||||
|
Params?: string
|
||||||
|
}
|
||||||
@@ -106,5 +106,9 @@ export function toFormatArgs(codes: string[]): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getWebSocketEndpoint() {
|
export function getWebSocketEndpoint() {
|
||||||
return `${window.location.protocol}//${localStorage.getItem('server-addr') || window.location.hostname}:${localStorage.getItem('server-port') || window.location.port}`
|
return `ws://${localStorage.getItem('server-addr') || window.location.hostname}:${localStorage.getItem('server-port') || window.location.port}/ws-rpc`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHttpRPCEndpoint() {
|
||||||
|
return `${window.location.protocol}//${localStorage.getItem('server-addr') || window.location.hostname}:${localStorage.getItem('server-port') || window.location.port}/http-rpc`
|
||||||
}
|
}
|
||||||
@@ -33,20 +33,31 @@ func RunBlocking(ctx context.Context) {
|
|||||||
Root: http.FS(fe),
|
Root: http.FS(fe),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
app.Get("/ws", websocket.New(func(c *websocket.Conn) {
|
app.Get("/ws-rpc", websocket.New(func(c *websocket.Conn) {
|
||||||
for {
|
for {
|
||||||
mtype, reader, err := c.NextReader()
|
mtype, reader, err := c.NextReader()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
res := NewRPCRequest(reader).Call()
|
||||||
|
|
||||||
writer, err := c.NextWriter(mtype)
|
writer, err := c.NextWriter(mtype)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
res := NewRPCRequest(reader).Call()
|
|
||||||
io.Copy(writer, res)
|
io.Copy(writer, res)
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
app.Post("/http-rpc", func(c *fiber.Ctx) error {
|
||||||
|
reader := c.Context().RequestBodyStream()
|
||||||
|
writer := c.Response().BodyWriter()
|
||||||
|
res := NewRPCRequest(reader).Call()
|
||||||
|
io.Copy(writer, res)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
app.Server().StreamRequestBody = true
|
||||||
|
|
||||||
log.Fatal(app.Listen(fmt.Sprintf(":%s", port)))
|
log.Fatal(app.Listen(fmt.Sprintf(":%s", port)))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user