Skip to content

Commit

Permalink
add itch.io upload action
Browse files Browse the repository at this point in the history
  • Loading branch information
Armaldio committed Jan 14, 2025
1 parent a86cebe commit e6207d0
Show file tree
Hide file tree
Showing 4 changed files with 257 additions and 26 deletions.
68 changes: 64 additions & 4 deletions src/shared/libs/plugin-core/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Options, Subprocess } from 'execa'
import { IPty, type IPtyForkOptions, type IWindowsPtyForkOptions } from '@lydell/node-pty'
import { createWriteStream } from 'fs'
import { pipeline } from 'stream/promises'

export const runWithLiveLogs = async (
command: string,
Expand All @@ -21,16 +23,14 @@ export const runWithLiveLogs = async (
...execaOptions,
stdout: 'pipe',
stderr: 'pipe',
stdin: 'pipe',
stdin: 'pipe'
})

subprocess.stdout.on('data', (data: Buffer) => {
log(data.toString())
hooks?.onStdout?.(data.toString(), subprocess)
})

subprocess.stderr?.on('data', (data: Buffer) => {
log(data.toString())
hooks?.onStderr?.(data.toString(), subprocess)
})

Expand Down Expand Up @@ -88,7 +88,6 @@ export const runWithLiveLogsPTY = async (
const subprocess = spawn(command, args, ptyOptions)

subprocess.onData((data) => {
log(data.toString())
hooks?.onStdout?.(data.toString(), subprocess)
})

Expand All @@ -104,3 +103,64 @@ export const runWithLiveLogsPTY = async (
})
})
}

export interface Hooks {
onProgress?: (data: { progress: number; downloadedSize: number }) => void
}

/**
* Downloads a file from a given URL to a specified local path with progress tracking.
*
* @param url - The URL of the file to download.
* @param localPath - The local file path to save the downloaded file.
* @returns A promise that resolves when the file is downloaded.
*/
export const downloadFile = async (
url: string,
localPath: string,
hooks?: Hooks
): Promise<void> => {
// Fetch the resource
const response = await fetch(url)

// Check if the fetch was successful
if (!response.ok) {
throw new Error(`Failed to fetch file: ${response.statusText}`)
}

// Get the total size of the file
const contentLength = response.headers.get('content-length')
if (!contentLength) {
throw new Error('Content-Length header is missing')
}
const totalSize = parseInt(contentLength, 10)

// Track progress
let downloadedSize = 0

// Create a write stream for the file
const fileStream = createWriteStream(localPath)

// Create a readable stream to monitor progress
const progressStream = new TransformStream({
transform(chunk, controller) {
downloadedSize += chunk.length
const progress = (downloadedSize / totalSize) * 100
if (hooks.onProgress) {
hooks.onProgress({
progress,
downloadedSize
})
}
controller.enqueue(chunk)
}
})

// Pipe the response through the progress tracker and into the file
const readable = response.body?.pipeThrough(progressStream)
if (!readable) {
throw new Error('Failed to create a readable stream')
}

await pipeline(readable, fileStream)
}
186 changes: 166 additions & 20 deletions src/shared/libs/plugin-itch/export.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,40 @@
import { createAction, createActionRunner } from "@pipelab/plugin-core";
import {
createAction,
createActionRunner,
downloadFile,
runWithLiveLogs
} from '@pipelab/plugin-core'

export const ID = "itch-upload";
export const ID = 'itch-upload'

export interface ButlerJSONOutputLog {
level: 'info'
message: string
time: number
type: 'log'
}

export interface ButlerJSONOutputProgress {
bps: number
eta: number
progress: number
time: 1736873335
type: 'progress'
}

export type ButlerJSONOutput = ButlerJSONOutputLog | ButlerJSONOutputProgress

export const uploadToItch = createAction({
id: ID,
name: "Upload to Itch.io",
description: "",
icon: "",
displayString: 'TODO',
name: 'Upload to Itch.io',
description: '',
icon: '',
displayString:
"`Upload ${fmt.param(params['input-folder'], 'primary', 'No path selected')} to ${fmt.param(params['user'], 'primary', 'No project')}/${fmt.param(params['project'], 'primary', 'No project')}:${fmt.param(params['channel'], 'primary', 'No channel')}`",
meta: {},
params: {
"input-folder": {
label: "Folder to Upload",
'input-folder': {
label: 'Folder to Upload',
value: '',
control: {
type: 'path',
Expand All @@ -20,8 +43,8 @@ export const uploadToItch = createAction({
}
}
},
project: {
label: "Project",
user: {
label: 'User',
value: '',
control: {
type: 'input',
Expand All @@ -30,8 +53,8 @@ export const uploadToItch = createAction({
}
}
},
channel: {
label: "Channel",
project: {
label: 'Project',
value: '',
control: {
type: 'input',
Expand All @@ -40,8 +63,8 @@ export const uploadToItch = createAction({
}
}
},
"api-key": {
label: "API key",
channel: {
label: 'Channel',
value: '',
control: {
type: 'input',
Expand All @@ -50,11 +73,134 @@ export const uploadToItch = createAction({
}
}
},
'api-key': {
label: 'API key',
value: '',
control: {
type: 'input',
options: {
kind: 'text',
hideChars: true
}
}
}
},
outputs: {
},
});

export const uploadToItchRunner = createActionRunner(async ({ log }) => {
log("uploading to itch");
outputs: {}
})

export const uploadToItchRunner = createActionRunner<typeof uploadToItch>(
async ({ log, inputs, cwd }) => {
const { app } = await import('electron')
const { join, dirname } = await import('node:path')
const { mkdir, access, chmod } = await import('node:fs/promises')
const StreamZip = await import('node-stream-zip')

const userData = app.getPath('userData')

const itchMetadataPath = join(userData, 'thirdparty', 'itch')
const butlerTmpZipFile = join(cwd, 'thirdparty', 'itch', 'butler.zip')

log('butlerTmpZipFile', butlerTmpZipFile)

// create destination dir
await mkdir(itchMetadataPath, {
recursive: true
})

// create tmp dir
await mkdir(dirname(butlerTmpZipFile), {
recursive: true
})

const localOs = process.platform
const localArch = process.arch

let butlerName = ''
if (localOs === 'darwin') {
butlerName += 'darwin'
} else if (localOs === 'linux') {
butlerName += 'linux'
} else if (localOs === 'win32') {
butlerName += 'windows'
}

butlerName += '-'

if (localArch === 'x64') {
butlerName += 'amd64'
} else {
throw new Error('Unsupported architecture')
}

let extension = ''
if (localOs === 'win32') {
extension += '.exe'
}

const butlerPath = join(itchMetadataPath, `butler${extension}`)
console.log('butlerPath', butlerPath)

let alreadyExist = true

try {
await access(butlerPath)
} catch (e) {
alreadyExist = false
}

const url = `https://broth.itch.zone/butler/${butlerName}/LATEST/archive/default`
console.log('url', url)

if (alreadyExist === false) {
await downloadFile(url, butlerTmpZipFile, {
onProgress: ({ progress }) => {
log(`Downloading itch.io butler: ${progress.toFixed(2)}%`)
}
})
const zip = new StreamZip.default.async({ file: butlerTmpZipFile })

const bytes = await zip.extract(null, dirname(butlerPath))
await zip.close()

log('bytes', bytes)
}

await chmod(butlerPath, 0o755)

log('Uploading to itch')

await runWithLiveLogs(
butlerPath,
[
'push',
inputs['input-folder'],
`${inputs.user}/${inputs.project}:${inputs.channel}`,
'--json'
],
{
env: {
BUTLER_API_KEY: inputs['api-key']
}
},
log,
{
onStdout(data, subprocess) {
const jsons = data.trim().split('\n')
for (const jsonData of jsons) {
const json = JSON.parse(jsonData) as ButlerJSONOutput
switch (json.type) {
case 'log':
log(json.message)
break
case 'progress':
log(`${json.progress}% - ETA: ${json.eta}s`)
break
}
}
}
}
)

log('Uploaded to itch')
}
)
26 changes: 25 additions & 1 deletion src/shared/libs/plugin-itch/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,25 @@
export * as Export from './export.js'
import { uploadToItch, uploadToItchRunner } from './export'

import { createNodeDefinition } from '@pipelab/plugin-core'
// import icon from './public/itch.webp'

export default createNodeDefinition({
description: 'Itch.io',
name: 'Itch.io',
id: 'itch.io',
icon: {
type: 'icon',
icon: 'mdi-home'
},
// icon: {
// type: 'image',
// image: icon
// },
nodes: [
// make and package
{
node: uploadToItch,
runner: uploadToItchRunner
}
]
})
3 changes: 2 additions & 1 deletion src/shared/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ const builtInPlugins = async () => {
(await import('../shared/libs/plugin-filesystem')).default,
(await import('../shared/libs/plugin-system')).default,
(await import('../shared/libs/plugin-steam')).default,
(await import('../shared/libs/plugin-itch')).default,
(await import('../shared/libs/plugin-electron')).default,
(await import('../shared/libs/plugin-tauri')).default
// (await import('../shared/libs/plugin-tauri')).default
])
).flat()
}
Expand Down

0 comments on commit e6207d0

Please sign in to comment.