├── .eslintrc.json ├── .prettierrc ├── assets └── logo.png ├── README.md ├── src ├── shared │ ├── preferences.ts │ ├── storage.ts │ └── upload.ts ├── index.ts ├── copyLastUploadURL.ts ├── uploadFromClipboard.ts └── viewHistory.tsx ├── .gitignore ├── CHANGELOG.md ├── tsconfig.json └── package.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": ["@raycast"] 4 | } 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": false 4 | } 5 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0PandaDEV/streamshare-uploader/main/assets/logo.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Streamshare Uploader 2 | 3 | With this extension, you can upload files to [streamshare](https://streamshare.wireway.ch) directly from Raycast. 4 | 5 | The max upload limit is 1.3TB yes Terabytes. 6 | -------------------------------------------------------------------------------- /src/shared/preferences.ts: -------------------------------------------------------------------------------- 1 | import { getPreferenceValues } from "@raycast/api"; 2 | 3 | interface Preferences { 4 | copyUrlToClipboard: boolean; 5 | } 6 | 7 | export function getPreferences(): Preferences { 8 | return getPreferenceValues(); 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # Raycast specific files 7 | raycast-env.d.ts 8 | .raycast-swift-build 9 | .swiftpm 10 | compiled_raycast_swift 11 | 12 | # misc 13 | .DS_Store 14 | bun.lock 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Streamshare Uploader Changelog 2 | 3 | ## [v1.2] - 2025-02-15 4 | 5 | - Add Chunked uploading 6 | 7 | ## [v1.1] - 2025-02-14 8 | 9 | - Rename "Upload File" command to "Upload Selected File" 10 | - Add "Copy Last Upload URL" command 11 | - Add "Upload File from Clipboard" command 12 | - Add "View Upload History" command 13 | - Add option to disable copying file URL to clipboard when upload completes 14 | 15 | ## [v1.0] - 2024-07-23 16 | 17 | - Initial release 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "include": ["src/**/*", "raycast-env.d.ts"], 4 | "compilerOptions": { 5 | "lib": ["ES2023"], 6 | "module": "commonjs", 7 | "target": "ES2022", 8 | "strict": true, 9 | "isolatedModules": true, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "jsx": "react-jsx", 14 | "resolveJsonModule": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { showToast, Toast, getSelectedFinderItems } from "@raycast/api"; 2 | import { uploadFile } from "./shared/upload"; 3 | import fs from "fs"; 4 | 5 | export default async function Command() { 6 | try { 7 | const selectedItems = await getSelectedFinderItems(); 8 | 9 | if (selectedItems.length === 0) { 10 | await showToast({ title: "No file selected", style: Toast.Style.Failure }); 11 | return; 12 | } 13 | 14 | if (!fs.statSync(selectedItems[0].path).isFile()) { 15 | await showToast({ title: "Selected item is not a file", style: Toast.Style.Failure }); 16 | return; 17 | } 18 | 19 | await uploadFile(selectedItems[0].path); 20 | } catch (error) { 21 | await showToast({ 22 | title: "Error selecting file", 23 | message: error instanceof Error ? error.message : String(error), 24 | style: Toast.Style.Failure, 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/copyLastUploadURL.ts: -------------------------------------------------------------------------------- 1 | import { showToast, Toast, Clipboard } from "@raycast/api"; 2 | import { getUploadHistory } from "./shared/storage"; 3 | 4 | export default async function Command() { 5 | try { 6 | const history = await getUploadHistory(); 7 | 8 | if (history.length === 0) { 9 | await showToast({ 10 | style: Toast.Style.Failure, 11 | title: "No uploads found", 12 | }); 13 | return; 14 | } 15 | 16 | const lastUpload = history[0]; 17 | await Clipboard.copy(lastUpload.downloadUrl); 18 | 19 | await showToast({ 20 | style: Toast.Style.Success, 21 | title: "Copied last upload URL", 22 | message: lastUpload.sourceFileName, 23 | }); 24 | } catch (error) { 25 | await showToast({ 26 | style: Toast.Style.Failure, 27 | title: "Failed to copy URL", 28 | message: error instanceof Error ? error.message : String(error), 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/shared/storage.ts: -------------------------------------------------------------------------------- 1 | import { LocalStorage } from "@raycast/api"; 2 | 3 | export interface UploadRecord { 4 | sourceFileName: string; 5 | downloadUrl: string; 6 | deletionUrl: string; 7 | timestamp: number; 8 | } 9 | 10 | export async function saveUploadRecord(record: UploadRecord) { 11 | const history = await getUploadHistory(); 12 | history.unshift(record); 13 | await LocalStorage.setItem("upload-history", JSON.stringify(history)); 14 | } 15 | 16 | export async function getUploadHistory(): Promise { 17 | const historyStr = await LocalStorage.getItem("upload-history"); 18 | return historyStr ? JSON.parse(historyStr as string) : []; 19 | } 20 | 21 | export async function removeUploadRecord(downloadUrl: string) { 22 | const history = await getUploadHistory(); 23 | const updatedHistory = history.filter((record) => record.downloadUrl !== downloadUrl); 24 | await LocalStorage.setItem("upload-history", JSON.stringify(updatedHistory)); 25 | } 26 | 27 | export async function clearUploadHistory() { 28 | await LocalStorage.setItem("upload-history", JSON.stringify([])); 29 | } 30 | -------------------------------------------------------------------------------- /src/uploadFromClipboard.ts: -------------------------------------------------------------------------------- 1 | import { showToast, Toast, Clipboard } from "@raycast/api"; 2 | import fs from "fs"; 3 | import { uploadFile } from "./shared/upload"; 4 | 5 | async function getClipboardFilePath(): Promise { 6 | function normalizeFilePath(filePath: string): string { 7 | // Remove file:// protocol if present 8 | let normalizedPath = filePath.replace(/^file:\/\//, ""); 9 | 10 | // Decode URL-encoded characters 11 | normalizedPath = decodeURIComponent(normalizedPath); 12 | 13 | return normalizedPath; 14 | } 15 | 16 | // Try clipboard for file path text 17 | const clipboardText = await Clipboard.readText(); 18 | 19 | if (clipboardText) { 20 | const normalizedPath = normalizeFilePath(clipboardText); 21 | 22 | // Check if it's a valid file path 23 | if (fs.existsSync(normalizedPath) && fs.statSync(normalizedPath).isFile()) { 24 | return normalizedPath; 25 | } 26 | } 27 | 28 | // Try clipboard for copied file 29 | const clipboardFile = await Clipboard.read(); 30 | 31 | if (clipboardFile.file) { 32 | const normalizedPath = normalizeFilePath(clipboardFile.file); 33 | 34 | if (fs.existsSync(normalizedPath)) { 35 | return normalizedPath; 36 | } 37 | } 38 | 39 | return null; 40 | } 41 | 42 | export default async function Command() { 43 | const filePath = await getClipboardFilePath(); 44 | 45 | try { 46 | if (!filePath) { 47 | await showToast({ title: "No file found on clipboard", style: Toast.Style.Failure }); 48 | return; 49 | } 50 | 51 | await uploadFile(filePath); 52 | } catch (error) { 53 | await showToast({ 54 | title: "Error getting file from clipboard", 55 | message: error instanceof Error ? error.message : String(error), 56 | style: Toast.Style.Failure, 57 | }); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://www.raycast.com/schemas/extension.json", 3 | "name": "streamshare-uploader", 4 | "title": "Streamshare Uploader", 5 | "description": "Upload files to streamshare", 6 | "icon": "logo.png", 7 | "author": "PandaDEV", 8 | "contributors": [ 9 | "britown" 10 | ], 11 | "categories": [ 12 | "Data", 13 | "Developer Tools", 14 | "Productivity" 15 | ], 16 | "license": "MIT", 17 | "commands": [ 18 | { 19 | "name": "index", 20 | "title": "Upload Selected File", 21 | "description": "Upload a file selected in Finder", 22 | "mode": "no-view" 23 | }, 24 | { 25 | "name": "uploadFromClipboard", 26 | "title": "Upload File from Clipboard", 27 | "description": "Upload a file from clipboard (path or copied file)", 28 | "mode": "no-view" 29 | }, 30 | { 31 | "name": "viewHistory", 32 | "title": "View Upload History", 33 | "description": "View and manage your uploaded files", 34 | "mode": "view" 35 | }, 36 | { 37 | "name": "copyLastUploadURL", 38 | "title": "Copy Last Upload URL", 39 | "description": "Copy the URL of the most recently uploaded file", 40 | "mode": "no-view" 41 | } 42 | ], 43 | "preferences": [ 44 | { 45 | "name": "copyUrlToClipboard", 46 | "type": "checkbox", 47 | "required": false, 48 | "title": "Copy URL to Clipboard", 49 | "description": "Automatically copy URL to clipboard when an upload completes", 50 | "default": true, 51 | "label": "Copy URL to Clipboard" 52 | } 53 | ], 54 | "dependencies": { 55 | "@raycast/api": "^1.91.2", 56 | "archiver": "^7.0.1", 57 | "axios": "^1.12.0", 58 | "file-type": "^20.1.0", 59 | "form-data": "^4.0.2", 60 | "ws": "^8.18.0" 61 | }, 62 | "devDependencies": { 63 | "@raycast/eslint-config": "^1.0.8", 64 | "@types/archiver": "^6.0.3", 65 | "@types/node": "22.13.4", 66 | "@types/react": "^18.3.18", 67 | "@types/ws": "^8.5.14", 68 | "eslint": "^8.57.1", 69 | "prettier": "^3.5.1", 70 | "typescript": "^5.7.3" 71 | }, 72 | "scripts": { 73 | "build": "ray build -e dist", 74 | "dev": "ray develop", 75 | "fix-lint": "ray lint --fix", 76 | "lint": "ray lint", 77 | "prepublishOnly": "echo \"\\n\\nIt seems like you are trying to publish the Raycast extension to npm.\\n\\nIf you did intend to publish it to npm, remove the \\`prepublishOnly\\` script and rerun \\`npm publish\\` again.\\nIf you wanted to publish it to the Raycast Store instead, use \\`npm run publish\\` instead.\\n\\n\" && exit 1", 78 | "publish": "npx @raycast/api@latest publish" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/shared/upload.ts: -------------------------------------------------------------------------------- 1 | import { showToast, Toast, launchCommand, LaunchType, Clipboard } from "@raycast/api"; 2 | import { getPreferences } from "./preferences"; 3 | import fs from "fs"; 4 | import path from "path"; 5 | import axios from "axios"; 6 | import { saveUploadRecord } from "./storage"; 7 | import { fileTypeFromFile } from "file-type"; 8 | 9 | const CHUNK_SIZE = 4 * 1024 * 1024; 10 | 11 | export async function uploadFile(filePath: string) { 12 | let fileName = path.basename(filePath); 13 | const fileSize = fs.statSync(filePath).size; 14 | 15 | if (!path.extname(fileName)) { 16 | try { 17 | const fileType = await fileTypeFromFile(filePath); 18 | 19 | if (fileType) { 20 | fileName = `${fileName}.${fileType.ext}`; 21 | } 22 | } catch (error) { 23 | // Continue with original filename if detection fails 24 | } 25 | } 26 | 27 | try { 28 | const totalChunks = Math.ceil(fileSize / CHUNK_SIZE); 29 | const createResponse = await axios.post("https://streamshare.wireway.ch/api/create", { 30 | name: fileName, 31 | totalChunks, 32 | }); 33 | const { fileIdentifier, deletionToken } = createResponse.data; 34 | 35 | const toast = await showToast({ 36 | style: Toast.Style.Animated, 37 | title: `Uploading ${fileName}`, 38 | message: "0%", 39 | }); 40 | 41 | const fileStream = fs.createReadStream(filePath, { highWaterMark: CHUNK_SIZE }); 42 | let uploadedSize = 0; 43 | let chunkId = 0; 44 | 45 | for await (const chunk of fileStream) { 46 | await axios.post(`https://streamshare.wireway.ch/api/upload/${fileIdentifier}/${chunkId}`, chunk, { 47 | headers: { 48 | "Content-Type": "application/octet-stream", 49 | "Content-Length": chunk.length, 50 | }, 51 | }); 52 | chunkId++; 53 | uploadedSize += chunk.length; 54 | const percentCompleted = Math.round((uploadedSize * 100) / fileSize); 55 | toast.message = `${percentCompleted}%`; 56 | } 57 | 58 | await axios.post(`https://streamshare.wireway.ch/api/upload-complete/${fileIdentifier}`); 59 | 60 | const downloadUrl = `https://streamshare.wireway.ch/download/${fileIdentifier}`; 61 | const deletionUrl = `https://streamshare.wireway.ch/api/delete/${fileIdentifier}/${deletionToken}`; 62 | const preferences = getPreferences(); 63 | 64 | if (preferences.copyUrlToClipboard) { 65 | await Clipboard.copy(downloadUrl); 66 | } 67 | 68 | const uploadRecord = { 69 | sourceFileName: fileName, 70 | downloadUrl, 71 | deletionUrl, 72 | timestamp: Date.now(), 73 | }; 74 | await saveUploadRecord(uploadRecord); 75 | 76 | await showToast({ 77 | style: Toast.Style.Success, 78 | title: preferences.copyUrlToClipboard ? "Copied URL to clipboard" : "Upload complete", 79 | message: fileName, 80 | primaryAction: { 81 | title: "View Uploads", 82 | onAction: () => launchCommand({ name: "viewHistory", type: LaunchType.UserInitiated }), 83 | }, 84 | }); 85 | } catch (error) { 86 | await showToast({ 87 | title: `Failed to upload ${fileName}`, 88 | message: error instanceof Error ? error.message : String(error), 89 | style: Toast.Style.Failure, 90 | }); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/viewHistory.tsx: -------------------------------------------------------------------------------- 1 | import { ActionPanel, Action, List, showToast, Toast, Icon } from "@raycast/api"; 2 | import { useState, useEffect } from "react"; 3 | import { getUploadHistory, removeUploadRecord, clearUploadHistory, type UploadRecord } from "./shared/storage"; 4 | import axios from "axios"; 5 | 6 | export default function Command() { 7 | const { data: history, revalidate } = useUploadHistory(); 8 | 9 | return ( 10 | 11 | {history.length === 0 ? ( 12 | 13 | ) : ( 14 | history.map((record) => ( 15 | 21 | 22 | 23 | { 27 | try { 28 | const toast = await showToast({ 29 | style: Toast.Style.Animated, 30 | title: "Checking file status...", 31 | }); 32 | 33 | await axios.head(record.downloadUrl); 34 | 35 | toast.style = Toast.Style.Success; 36 | toast.title = "File is available"; 37 | } catch (error) { 38 | await showToast({ 39 | style: Toast.Style.Failure, 40 | title: "File is no longer available", 41 | }); 42 | } 43 | }} 44 | /> 45 | { 50 | try { 51 | await axios.get(record.deletionUrl); 52 | await removeUploadRecord(record.downloadUrl); 53 | await revalidate(); 54 | await showToast({ 55 | style: Toast.Style.Success, 56 | title: "File deleted successfully", 57 | }); 58 | } catch (error) { 59 | await showToast({ 60 | style: Toast.Style.Failure, 61 | title: "Failed to delete file", 62 | message: error instanceof Error ? error.message : String(error), 63 | }); 64 | } 65 | }} 66 | /> 67 | { 72 | await removeUploadRecord(record.downloadUrl); 73 | await revalidate(); 74 | }} 75 | /> 76 | { 80 | try { 81 | await clearUploadHistory(); 82 | await revalidate(); 83 | await showToast({ 84 | style: Toast.Style.Success, 85 | title: "Upload history cleared", 86 | }); 87 | } catch (error) { 88 | await showToast({ 89 | style: Toast.Style.Failure, 90 | title: "Failed to clear history", 91 | message: error instanceof Error ? error.message : String(error), 92 | }); 93 | } 94 | }} 95 | /> 96 | 97 | } 98 | /> 99 | )) 100 | )} 101 | 102 | ); 103 | } 104 | 105 | function useUploadHistory() { 106 | const [data, setData] = useState([]); 107 | 108 | async function fetchHistory() { 109 | const history = await getUploadHistory(); 110 | setData(history); 111 | } 112 | 113 | useEffect(() => { 114 | fetchHistory(); 115 | }, []); 116 | 117 | return { data, revalidate: fetchHistory }; 118 | } 119 | --------------------------------------------------------------------------------