Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,13 +120,18 @@ gdcli <email> upload <localPath> [options]
Options:
- `--name <n>` - Override filename
- `--folder <folderId>` - Destination folder
- `--convert <type>` - Convert to Google format: `docs`, `sheets`, or `slides`

Examples:
```bash
gdcli you@gmail.com upload ./report.pdf
gdcli you@gmail.com upload ./report.pdf --folder 1ABC123 --name "Q4 Report.pdf"
gdcli you@gmail.com upload ./README.md --convert docs
gdcli you@gmail.com upload ./data.csv --convert sheets
```

**Note:** The `--convert` option uses Google Drive's import feature. Some conversions (notably Markdown to Google Docs with full formatting) work best with Google Workspace accounts. Personal Gmail accounts may have limited conversion support.

### mkdir

Create a folder.
Expand Down
7 changes: 6 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ DRIVE COMMANDS
Options:
--name <n> Override filename
--folder <folderId> Destination folder
--convert <type> Convert to Google format: docs, sheets, or slides

gdcli <email> mkdir <name> [--parent <folderId>]
Create a folder.
Expand Down Expand Up @@ -84,6 +85,7 @@ EXAMPLES
gdcli you@gmail.com download 1ABC123
gdcli you@gmail.com download 1ABC123 ./myfile.pdf
gdcli you@gmail.com upload ./report.pdf --folder 1ABC123
gdcli you@gmail.com upload ./README.md --convert docs
gdcli you@gmail.com mkdir "New Folder" --parent 1ABC123
gdcli you@gmail.com delete 1ABC123
gdcli you@gmail.com move 1ABC123 1DEF456
Expand Down Expand Up @@ -341,12 +343,14 @@ async function handleUpload(account: string, args: string[]) {
options: {
name: { type: "string" },
folder: { type: "string" },
convert: { type: "string" },
},
allowPositionals: true,
});

const localPath = positionals[0];
if (!localPath) error("Usage: <email> upload <localPath> [--name <n>] [--folder <folderId>]");
if (!localPath)
error("Usage: <email> upload <localPath> [--name <n>] [--folder <folderId>] [--convert docs|sheets|slides]");

if (!fs.existsSync(localPath)) {
error(`File not found: ${localPath}`);
Expand All @@ -355,6 +359,7 @@ async function handleUpload(account: string, args: string[]) {
const file = await service.upload(account, localPath, {
name: values.name,
folderId: values.folder,
convertTo: values.convert,
});

console.log(`Uploaded: ${file.id}`);
Expand Down
55 changes: 47 additions & 8 deletions src/drive-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,15 +206,23 @@ export class DriveService {
async upload(
email: string,
localPath: string,
options: { name?: string; folderId?: string; mimeType?: string } = {},
options: { name?: string; folderId?: string; mimeType?: string; convertTo?: string } = {},
): Promise<DriveFile> {
const drive = this.getDriveClient(email);

const fileName = options.name || path.basename(localPath);
const mimeType = options.mimeType || this.guessMimeType(localPath);

// Map convert option to Google Workspace mimeType
const targetMimeType = options.convertTo ? this.getConvertMimeType(options.convertTo) : undefined;

// Strip extension when converting, but preserve dotfiles (e.g., .env)
const { name: baseName, ext } = path.parse(fileName);
const uploadName = targetMimeType && ext ? baseName : fileName;

const fileMetadata: drive_v3.Schema$File = {
name: fileName,
name: uploadName,
mimeType: targetMimeType,
parents: options.folderId ? [options.folderId] : undefined,
};

Expand All @@ -223,13 +231,43 @@ export class DriveService {
body: fs.createReadStream(localPath),
};

const response = await drive.files.create({
requestBody: fileMetadata,
media,
fields: "id, name, mimeType, size, webViewLink",
});
try {
const response = await drive.files.create({
requestBody: fileMetadata,
media,
fields: "id, name, mimeType, size, webViewLink",
});

return response.data;
return response.data;
} catch (e) {
// Provide clearer error for conversion failures
if (options.convertTo && e instanceof Error) {
const status = (e as NodeJS.ErrnoException & { code?: number }).code;
if (status === 400 || status === 403) {
throw new Error(
`Conversion to ${options.convertTo} failed: ${e.message}. ` +
"Note: Some conversions (e.g., Markdown to Docs) require a Google Workspace account.",
);
}
}
throw e;
}
}

private getConvertMimeType(convertTo: string): string {
const mimeTypes: Record<string, string> = {
docs: "application/vnd.google-apps.document",
doc: "application/vnd.google-apps.document",
sheets: "application/vnd.google-apps.spreadsheet",
sheet: "application/vnd.google-apps.spreadsheet",
slides: "application/vnd.google-apps.presentation",
slide: "application/vnd.google-apps.presentation",
};
const result = mimeTypes[convertTo.toLowerCase()];
if (!result) {
throw new Error(`Unknown convert type: ${convertTo}. Use: docs, sheets, or slides`);
}
return result;
}

private guessMimeType(filePath: string): string {
Expand All @@ -254,6 +292,7 @@ export class DriveService {
".zip": "application/zip",
".csv": "text/csv",
".md": "text/markdown",
".mdx": "text/markdown",
};
return mimeTypes[ext] || "application/octet-stream";
}
Expand Down