Skip to content

Commit 24b35c1

Browse files
Upload progress bar on CLI (#1730)
Adds progress bars to file uploads. Adds `--xet/-x` flag to use xet for uploads, defaults to false <img width="1207" height="158" alt="Screenshot 2025-09-04 at 5 15 29 PM" src="https://github.com/user-attachments/assets/55f2003e-887f-4085-bcda-5586e9bc9474" /> <img width="1206" height="151" alt="Screenshot 2025-09-05 at 8 51 48 AM" src="https://github.com/user-attachments/assets/ea357ca0-24bd-40d1-9f32-e3119a4b0dde" /> --------- Co-authored-by: Eliott C. <[email protected]> Co-authored-by: coyotte508 <[email protected]>
1 parent fc4764e commit 24b35c1

File tree

4 files changed

+217
-11
lines changed

4 files changed

+217
-11
lines changed

packages/hub/cli.ts

Lines changed: 130 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,113 @@ import { stat } from "node:fs/promises";
88
import { basename, join } from "node:path";
99
import { HUB_URL } from "./src/consts";
1010
import { version } from "./package.json";
11+
import type { CommitProgressEvent } from "./src/lib/commit";
12+
import type { MultiBar, SingleBar } from "cli-progress";
13+
14+
// Progress bar manager for handling multiple file uploads
15+
class UploadProgressManager {
16+
private multibar: MultiBar | null = null;
17+
private fileBars: Map<string, SingleBar> = new Map();
18+
private readonly isQuiet: boolean;
19+
private cliProgressAvailable: boolean = false;
20+
21+
constructor(isQuiet: boolean = false) {
22+
this.isQuiet = isQuiet;
23+
}
24+
25+
async initialize(): Promise<void> {
26+
if (this.isQuiet) return;
27+
28+
try {
29+
const cliProgress = await import("cli-progress");
30+
this.cliProgressAvailable = true;
31+
this.multibar = new cliProgress.MultiBar(
32+
{
33+
clearOnComplete: false,
34+
hideCursor: true,
35+
format: " {bar} | {filename} | {percentage}% | {state}",
36+
barCompleteChar: "\u2588",
37+
barIncompleteChar: "\u2591",
38+
},
39+
cliProgress.Presets.shades_grey
40+
);
41+
} catch (error) {
42+
// cli-progress is not available, fall back to simple logging
43+
this.cliProgressAvailable = false;
44+
}
45+
}
46+
47+
handleEvent(event: CommitProgressEvent): void {
48+
if (this.isQuiet) return;
49+
50+
if (event.event === "phase") {
51+
this.logPhase(event.phase);
52+
} else if (event.event === "fileProgress") {
53+
this.updateFileProgress(event.path, event.progress, event.state);
54+
}
55+
}
56+
57+
private logPhase(phase: string): void {
58+
if (this.isQuiet) return;
59+
60+
const phaseMessages = {
61+
preuploading: "📋 Preparing files for upload...",
62+
uploadingLargeFiles: "⬆️ Uploading files...",
63+
committing: "✨ Finalizing commit...",
64+
};
65+
66+
console.log(`\n${phaseMessages[phase as keyof typeof phaseMessages] || phase}`);
67+
}
68+
69+
private updateFileProgress(path: string, progress: number, state: string): void {
70+
if (this.isQuiet) return;
71+
72+
if (this.cliProgressAvailable && this.multibar) {
73+
// Use progress bars
74+
let bar = this.fileBars.get(path);
75+
76+
if (!bar) {
77+
bar = this.multibar.create(100, 0, {
78+
filename: this.truncateFilename(path, 100),
79+
state: state,
80+
});
81+
this.fileBars.set(path, bar);
82+
}
83+
84+
if (progress >= 1) {
85+
// If complete, mark it as done
86+
bar.update(100, { state: state === "hashing" ? "✓ hashed" : "✓ uploaded" });
87+
} else {
88+
// Update the progress (convert 0-1 to 0-100)
89+
const percentage = Math.round(progress * 100);
90+
bar.update(percentage, { state: state });
91+
}
92+
} else {
93+
// Fall back to simple console logging
94+
const percentage = Math.round(progress * 100);
95+
const truncatedPath = this.truncateFilename(path, 100);
96+
97+
if (progress >= 1) {
98+
const statusIcon = state === "hashing" ? "✓ hashed" : "✓ uploaded";
99+
console.log(`${statusIcon}: ${truncatedPath}`);
100+
} else if (percentage % 25 === 0) {
101+
// Only log every 25% to avoid spam
102+
console.log(`${state}: ${truncatedPath} (${percentage}%)`);
103+
}
104+
}
105+
}
106+
107+
private truncateFilename(filename: string, maxLength: number): string {
108+
if (filename.length <= maxLength) return filename;
109+
return "..." + filename.slice(-(maxLength - 3));
110+
}
111+
112+
stop(): void {
113+
if (!this.isQuiet && this.cliProgressAvailable && this.multibar) {
114+
this.multibar.stop();
115+
}
116+
}
117+
}
11118

12119
// Didn't find the import from "node:util", so duplicated it here
13120
type OptionToken =
@@ -339,18 +446,31 @@ async function run() {
339446
]
340447
: [{ content: pathToFileURL(localFolder), path: pathInRepo.replace(/^[.]?\//, "") }];
341448

342-
for await (const event of uploadFilesWithProgress({
343-
repo: repoId,
344-
files,
345-
branch: revision,
346-
accessToken: token,
347-
commitTitle: commitMessage?.trim().split("\n")[0],
348-
commitDescription: commitMessage?.trim().split("\n").slice(1).join("\n").trim(),
349-
hubUrl: process.env.HF_ENDPOINT ?? HUB_URL,
350-
})) {
449+
const progressManager = new UploadProgressManager(!!quiet);
450+
await progressManager.initialize();
451+
452+
try {
453+
for await (const event of uploadFilesWithProgress({
454+
repo: repoId,
455+
files,
456+
branch: revision,
457+
accessToken: token,
458+
commitTitle: commitMessage?.trim().split("\n")[0],
459+
commitDescription: commitMessage?.trim().split("\n").slice(1).join("\n").trim(),
460+
hubUrl: process.env.HF_ENDPOINT ?? HUB_URL,
461+
useXet: true,
462+
})) {
463+
progressManager.handleEvent(event);
464+
}
465+
351466
if (!quiet) {
352-
console.log(event);
467+
console.log("\n✅ Upload completed successfully!");
353468
}
469+
} catch (error) {
470+
progressManager.stop();
471+
throw error;
472+
} finally {
473+
progressManager.stop();
354474
}
355475
break;
356476
}

packages/hub/package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,5 +66,11 @@
6666
"license": "MIT",
6767
"dependencies": {
6868
"@huggingface/tasks": "workspace:^"
69+
},
70+
"optionalDependencies": {
71+
"cli-progress": "^3.12.0"
72+
},
73+
"devDependencies": {
74+
"@types/cli-progress": "^3.11.6"
6975
}
7076
}

packages/hub/pnpm-lock.yaml

Lines changed: 80 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/hub/src/lib/commit.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@ export async function* commitIter(params: CommitParams): AsyncGenerator<CommitPr
363363

364364
const shaToOperation = new Map(operations.map((op, i) => [shas[i], op]));
365365

366-
if (params.useXet) {
366+
if (useXet) {
367367
// First get all the files that are already uploaded out of the way
368368
for (const obj of json.objects) {
369369
const op = shaToOperation.get(obj.oid);

0 commit comments

Comments
 (0)