degoo-api-js
v1.0.2
Published
TypeScript SDK for Degoo cloud storage API
Readme
degoo-api-js
An unofficial, TypeScript-first SDK for the Degoo cloud-storage API.
Heads-up. This SDK is built on Degoo's undocumented GraphQL API. It may break if Degoo changes their backend without notice.
Table of Contents
- Highlights
- Installation
- Quick Start
- Authentication
- API Reference
- Streaming Large Files
- Error Handling
- Configuration
- Session Stores
- Security
- TypeScript Types
- Known Limitations
- Architecture
- Author
- License
Highlights
- Authentication — email/password login with automatic session caching and silent token refresh.
- Browse — list, paginate, traverse the full tree, filter by category, search, list trash & shared.
- Mutate — rename, move, copy, delete, restore, hide/unhide, set description.
- Share — generate public links, share with users, revoke access, list shared items.
- Upload — single file with content-checksum deduplication; recursive directory mirror.
- Download — three-layered API:
download()to disk,downloadFileStream()to aReadable, or just resolve the presigned URL. - Large-file friendly — HTTP
Rangeresume,AbortSignalcancellation, socket-idle timeout, exponential-backoff retry. - Defence-in-depth — path-traversal guard, HTTPS-only redirects, SSRF block, symlink-safe uploads, atomic +
0o600session files, AES-256-GCM encrypted session storage. - Pluggable — swap the session store for Redis, memory, encrypted disk, or your own backend.
- TypeScript-first — complete typings for every input and output; stable
DegooErrorCodeenum for programmatic recovery.
Installation
npm install degoo-api-js
# or
pnpm add degoo-api-js
# or
yarn add degoo-api-jsRequirements: Node.js ≥ 16.
Quick Start
import { DegooClient, FileCategory } from 'degoo-api-js';
const client = await DegooClient.connect('[email protected]', 'password');
// List the root
const { files } = await client.listFiles();
files.forEach(f => console.log(f.Name, f.ID));
// Upload a file
const result = await client.upload('./photo.jpg');
console.log('Uploaded:', result.name, '— already existed:', result.alreadyExists);
// Download to disk with progress
await client.download(result.file!.ID, './downloads/', {
onProgress: (received, total) =>
process.stdout.write(`\r${received} / ${total ?? '?'} bytes`),
});
// Generate a public link
const url = await client.share(result.file!.ID);
console.log('Public link:', url);Authentication
DegooClient.connect(email, password, config?)
The recommended way to create a client. Logs in (or restores a cached session) and returns a ready-to-use DegooClient.
const client = await DegooClient.connect('[email protected]', 'password');Session tokens are cached to .degoo-session in the current working directory by default. On subsequent calls, the cached token is validated and re-used — no network login occurs unless the token has expired.
client.login(email, password)
Re-authenticates an existing instance — useful after a deliberate logout() or password change.
await client.login('[email protected]', 'new-password');client.logout()
Clears the in-memory token and removes the session file.
Accessors
client.token // current access token (string)
client.rootPathId // root folder ID for this account (string)API Reference
Profile
getProfile()
Returns the authenticated user's profile and storage quota.
const profile = await client.getProfile();
console.log(`${profile.FirstName} ${profile.LastName}`);
console.log(`Storage: ${profile.UsedQuota} / ${profile.TotalQuota} bytes`);
console.log(`Account type: ${profile.AccountType}`); // 1 = Free, 2 = ProReturns: UserProfile
Listing Files
listFiles(pathId?, options?)
Returns one page of files and folders from a directory.
// List root (virtual aggregator — read-only; see Known Limitations)
const { files, nextToken } = await client.listFiles();
// List a specific folder
const { files } = await client.listFiles('20877831487');
// Paginated
const page1 = await client.listFiles(undefined, { limit: 50 });
const page2 = await client.listFiles(undefined, { limit: 50, nextToken: page1.nextToken! });| Option | Type | Default | Description |
|---|---|---|---|
| limit | number | 100 | Items per page |
| nextToken | string | — | Cursor from a previous call |
| order | 1 \| 2 | 1 | 1 = ascending, 2 = descending |
Returns: FileListResult
listAll(pathId?)
Returns every item in a directory by automatically following all pages. Use with care on very large directories — the entire result is held in memory.
const allFiles = await client.listAll('20877831487');
console.log(`${allFiles.length} total items`);listByCategory(categories, options?)
Lists files filtered by content type across the entire account.
import { FileCategory } from 'degoo-api-js';
const { files } = await client.listByCategory([FileCategory.Photo, FileCategory.Video]);
const { files: docs } = await client.listByCategory([FileCategory.Document], { limit: 50 });FileCategory values: Folder | Photo | Video | Music | Document | Archive | Other.
| Option | Type | Default | Description |
|---|---|---|---|
| limit | number | 100 | Items per page |
| nextToken | string | — | Pagination cursor |
| minCreationTime | string | — | Unix timestamp (ms), lower bound |
| maxCreationTime | string | — | Unix timestamp (ms), upper bound |
listTrash(options?)
Lists files currently in the recycle bin.
File Detail
getFile(fileId) / getFileInfo(fileId)
Fetches complete metadata for a single file or folder, including a presigned download URL. getFileInfo is an alias exposed alongside the other download helpers — pick whichever reads better in context.
const detail = await client.getFile('20877831487');
console.log(detail.Name);
console.log(detail.Size); // bytes as string
console.log(detail.Category); // FileCategory number
console.log(detail.MetadataID); // needed for hide/unhide/setDescription
console.log(detail.IsHidden);
console.log(detail.IsInRecycleBin);
console.log(detail.URL); // presigned download URL (time-limited)
console.log(detail.Shareinfo); // { Status, ShareTime } or nullReturns: DegooFileDetail
Search
search(term, limit?)
Searches for files and folders by name across the entire account.
const results = await client.search('vacation', 20);searchPaginated(term, options?)
Paginated version of search for large result sets.
Folders
createDirectory(name, pathId?)
Creates a new empty folder.
const folder = await client.createDirectory('Backups', parentFolderId);
if (!folder) {
// Search index lag — retry after a short delay
await new Promise(r => setTimeout(r, 3000));
const [found] = await client.search('Backups', 1);
console.log('Created (delayed):', found.ID);
}Returns
nullwhen Degoo's search index hasn't caught up yet. See Known Limitations.
File Mutations
await client.rename([{ fileId: '123', newName: 'Vacation 2024.jpg' }]);
await client.move(['123', '456'], destFolderId);
await client.copy(['123'], backupFolderId);
await client.delete(['123', '456']); // recycle bin
await client.restore(['123']);
// Hidden flag and description require MetadataID, available via getFile().
const { MetadataID } = await client.getFile('123');
await client.hide(MetadataID!);
await client.unhide(MetadataID!);
await client.setDescription(MetadataID!, 'Summer in Bali');
delete()may fail with"Got empty result!"on files uploaded via the API — see Known Limitations.
Sharing
// Public link
const url = await client.share('123');
// Share with specific users
await client.shareWithUsers(['123'], ['[email protected]'], /* readOnly */ true);
// Revoke (omit usernames to revoke all sharing including the public link)
await client.unshare(['123']);
await client.unshare(['123'], ['[email protected]']);
// List
const { files: shared } = await client.getShared({ limit: 50 });
const sharedWithMe = await client.getSharedWithMe();Upload
upload(filePath, pathId?, filename?)
Uploads a local file to Degoo. Degoo deduplicates by content checksum; when alreadyExists is true, the S3 transfer is skipped but the metadata entry is still created.
const result = await client.upload('./photo.jpg', myDriveFolderId);
console.log(result.name); // stored filename
console.log(result.alreadyExists); // true if content was already in storage
console.log(result.file?.ID); // created file's ID (may lag the search index)If filePath is a directory, the entire tree is mirrored recursively — see uploadDirectory().
uploadDirectory(dirPath, pathId?)
Recursively mirrors a local directory tree into a Degoo folder.
await client.uploadDirectory('./photos/2024', targetFolderId);Symlinks are skipped by design — a symlink inside
dirPathcould point outside it (e.g.~/.ssh/id_rsa) and silently exfiltrate data. Pass linked targets explicitly viaupload(path)if you really want them uploaded.
Download
The download API has three layers, from highest- to lowest-level:
| Method | Returns | Use when |
|---|---|---|
| download(fileId, destDir, options?) | DownloadResult | Save the full file to disk with progress tracking. |
| downloadFileStream(fileId, options?) | DownloadStreamResult (Readable + metadata) | Pipe anywhere — HTTP response, transcoder, S3 multipart, etc. Supports resume and cancellation. |
| getFileDownloadUrl / getFileUrl | string / string \| null | Hand the presigned URL to another process or language. |
getFileUrl(fileId)
Returns the presigned URL, or null for folders.
const url = await client.getFileUrl('123');getFileDownloadUrl(fileId)
Stricter sibling of getFileUrl — throws DegooError(NoDownloadUrl) when no URL is available.
const url = await client.getFileDownloadUrl('123'); // never nulldownload(fileId, destDir, options?)
Saves the file to disk with automatic redirect following, partial-file cleanup on error, and built-in large-file safety knobs.
const result = await client.download('123', './downloads/', {
onProgress: (received, total) => {
const pct = total ? Math.round((received / total) * 100) : '?';
process.stdout.write(`\r${pct}% ${received} / ${total ?? '?'} bytes`);
},
// Inherited streaming-layer knobs:
timeoutMs: 30_000,
retries: 5,
signal: ctrl.signal,
});| Option | Type | Default | Description |
|---|---|---|---|
| filename | string | server-supplied | Override the local filename. |
| onProgress | (received, total?) => void | — | Progress callback. |
| signal | AbortSignal | — | Cancel the download mid-flight. |
| timeoutMs | number | 60_000 | Socket-inactivity timeout. |
| retries | number | 3 | Pre-body retries on transient errors. |
Returns: DownloadResult
downloadFileStream(fileId, options?)
Returns a Readable instead of writing to disk. See Streaming Large Files for the full API and patterns (resume, cancel, pipe-to-Express).
Streaming Large Files
downloadFileStream is built for multi-GB transfers where the simple "save-to-disk" path is too rigid. It exposes:
- HTTP
Rangesupport — resume after a network drop without re-downloading bytes you already have. AbortSignalcancellation — kill a download (and its socket) at any point.- Socket-idle timeout — connections stalled mid-stream are torn down instead of hanging.
- Exponential-backoff retry on the initial connect — transient
ECONNRESET/ 5xx are retried up tooptions.retriestimes. Mid-stream errors are surfaced to the caller (see "resume" below).
Pipe to disk with progress and cancellation
import fs from 'fs';
import { DegooClient } from 'degoo-api-js';
const client = await DegooClient.connect(email, password);
const ctrl = new AbortController();
const { stream, size } = await client.downloadFileStream(fileId, {
signal: ctrl.signal,
timeoutMs: 30_000,
});
let received = 0;
stream.on('data', (chunk: Buffer) => {
received += chunk.length;
process.stdout.write(`\r${received}/${size ?? '?'}`);
});
stream.pipe(fs.createWriteStream('./big.zip'));
// Cancel any time:
// ctrl.abort();Resume after a dropped connection
import fs from 'fs';
const dest = './big.iso';
const partial = fs.existsSync(dest) ? fs.statSync(dest).size : 0;
const { stream } = await client.downloadFileStream(fileId, {
range: { start: partial },
});
stream.pipe(fs.createWriteStream(dest, { flags: 'a' })); // appendStream straight to an Express response
app.get('/file/:id', async (req, res) => {
const info = await client.getFileInfo(req.params.id);
res.setHeader('Content-Length', info.Size);
res.setHeader('Content-Disposition', `attachment; filename="${info.Name}"`);
const { stream } = await client.downloadFileStream(req.params.id);
stream.pipe(res);
});Pipe to ffmpeg (transcode without touching disk)
import { spawn } from 'child_process';
const ff = spawn('ffmpeg', ['-i', 'pipe:0', '-c:v', 'libx264', 'out.mp4']);
const { stream } = await client.downloadFileStream(fileId);
stream.pipe(ff.stdin);DownloadStreamResult
interface DownloadStreamResult {
stream: NodeJS.ReadableStream; // pipe to anywhere
size?: number; // Content-Length (range length on a 206)
contentRange?: string; // raw Content-Range, present on 206
statusCode: number; // 200 or 206
url: string; // final URL after redirects
}Error Handling
All SDK methods throw DegooError on failure. Branch on instanceof DegooError, then on the stable DegooErrorCode enum or the HTTP status.
import { DegooClient, DegooError, DegooErrorCode } from 'degoo-api-js';
try {
await client.downloadFileStream(fileId);
} catch (err) {
if (!(err instanceof DegooError)) throw err;
switch (err.code) {
case DegooErrorCode.Unauthorized: return client.login(email, password);
case DegooErrorCode.Aborted: return; // user-cancelled
case DegooErrorCode.Timeout: console.warn('Connection stalled'); return;
case DegooErrorCode.NoDownloadUrl: console.warn('Folder, not a file'); return;
case DegooErrorCode.InvalidArgument: throw err; // programming error
}
if (err.status === 429) console.error('Rate limited');
else console.error(`Degoo error [${err.status ?? 'n/a'}]: ${err.message}`);
}DegooErrorCode
| Code | Meaning |
|---|---|
| Unauthorized | Auth missing, expired, or rejected. |
| Aborted | Operation cancelled via AbortSignal. |
| Timeout | Network operation exceeded its socket-idle timeout. |
| InvalidArgument | Caller supplied a bad argument (empty fileId, malformed range, escaping destDir, …). |
| NoDownloadUrl | File has no presigned URL (folder, expired session, server omitted it). |
| TooManyRedirects | Redirect chain exceeded the safety bound. |
| Network | Underlying transport failed (DNS, TLS, connection reset, …). |
| HttpStatus | Server returned a non-2xx HTTP status. |
DegooError properties
| Property | Type | Description |
|---|---|---|
| message | string | Human-readable description |
| status | number \| undefined | HTTP status if HTTP-derived |
| code | DegooErrorCode \| string \| undefined | Stable code for programmatic branching |
Configuration
Pass a DegooConfig to DegooClient.connect() or the constructor.
import { DegooClient, MemorySessionStore } from 'degoo-api-js';
const client = await DegooClient.connect('[email protected]', 'password', {
sessionStore: new MemorySessionStore(),
apiUrl: 'https://my-proxy.internal/graphql',
blockSize: 1024 * 1024, // 1 MB chunks for checksum streaming
});| Option | Type | Default | Description |
|---|---|---|---|
| apiUrl | string | Degoo AppSync endpoint | GraphQL API URL |
| loginUrl | string | Degoo REST login URL | Authentication endpoint |
| accessTokenUrl | string | Degoo token-exchange URL | Token-refresh endpoint |
| apiToken | string | Built-in AppSync key | x-api-key header value |
| userAgent | string | Built-in browser UA | User-Agent header |
| loginHeaders | Record<string, string> | Built-in auth headers | Extra headers merged into login requests |
| sessionStore | SessionStore | FileSessionStore | Session-persistence strategy |
| blockSize | number | 65_536 | Checksum streaming buffer size (bytes) |
Session Stores
FileSessionStore (default)
Persists tokens to a local file with 0o600 permissions and an atomic write (write-temp + rename) — symlink-safe and tolerant of concurrent token refreshes.
import { FileSessionStore } from 'degoo-api-js';
const store = new FileSessionStore('/var/data/.degoo-session');
const client = await DegooClient.connect(email, password, { sessionStore: store });EncryptedFileSessionStore
Persists tokens encrypted with AES-256-GCM (random 96-bit IV per save, 128-bit auth tag). Use this when the storage location may be readable by other users (shared dev boxes, CI runners, container images).
import { EncryptedFileSessionStore } from 'degoo-api-js';
// Static, per-deployment salt — at least 16 bytes.
const APP_SALT = Buffer.from('my-app-static-salt-v1', 'utf-8');
const key = EncryptedFileSessionStore.deriveKey(
process.env.DEGOO_SESSION_PASSPHRASE!,
APP_SALT,
);
const store = new EncryptedFileSessionStore('.degoo-session', key);
const client = await DegooClient.connect(email, password, { sessionStore: store });Wire format:
[1 byte version=1][12 bytes IV][16 bytes auth tag][ciphertext]Wrong key, modified ciphertext, or truncated tag all decode as "no session" — the SDK falls back to a full re-login, the safe default.
MemorySessionStore
Stores tokens in memory only. Suitable for short-lived scripts, lambdas, and tests.
import { MemorySessionStore } from 'degoo-api-js';
const client = await DegooClient.connect(email, password, {
sessionStore: new MemorySessionStore(),
});Custom stores
Implement SessionStore for Redis, Vault, KMS-wrapped disk, or anything else.
import type { SessionStore } from 'degoo-api-js';
import { createClient } from 'redis';
class RedisSessionStore implements SessionStore {
private redis = createClient();
private key = 'degoo:session';
async load() { return this.redis.get(this.key); }
async save(data: string) { await this.redis.set(this.key, data, { EX: 86400 }); }
async clear() { await this.redis.del(this.key); }
}Security
The SDK is hardened against common deployment risks. The current defaults guard against:
| Threat | Mitigation |
|---|---|
| Path traversal via attacker-controlled file.Name | download() resolves and verifies that the destination stays inside destDir; otherwise throws DegooError(InvalidArgument). |
| Plaintext tokens on shared disks | FileSessionStore writes with 0o600; EncryptedFileSessionStore adds AES-256-GCM with per-save IV. |
| Symlink overwrite of .degoo-session | Atomic write (rename) replaces the link itself, not its target. |
| Token-refresh race between processes | Atomic write — last rename wins, no torn files. |
| Symlink exfiltration during uploadDirectory | lstat is used; symlinks are skipped. |
| Redirect-driven SSRF (e.g. AWS metadata 169.254.169.254) | Redirect-following refuses localhost, RFC 1918, 127/8, link-local, IPv6 loopback / ULA. |
| HTTPS → HTTP redirect downgrade | Refused outright when the original URL was HTTPS. |
| Unbounded redirect chain | Hard cap of 10 redirects. |
| Unauthenticated tampering of encrypted session | GCM auth-tag verification fails closed → null → re-login. |
For shared / hostile environments the recommended posture is:
const store = new EncryptedFileSessionStore(
process.env.DEGOO_SESSION_PATH ?? '.degoo-session',
EncryptedFileSessionStore.deriveKey(
process.env.DEGOO_SESSION_PASSPHRASE!, // never embed in source
Buffer.from(process.env.DEGOO_SESSION_SALT!, 'utf-8'),
),
);Reporting security issues: open a private GitHub Security Advisory on the repository.
TypeScript Types
All types are exported from the package root.
DegooFile
interface DegooFile {
ID: string;
Name: string;
FilePath: string;
Size: string; // bytes as a numeric string — Number(file.Size) for arithmetic
URL: string; // presigned URL (may be empty in listing responses)
ThumbnailURL: string | null;
MetadataID?: string;
MetadataKey?: string;
LastModificationTime?: string;
ParentID?: string;
IsShared?: boolean;
}DegooFileDetail
interface DegooFileDetail extends DegooFile {
Category: number; // FileCategory enum value
IsHidden: boolean;
IsInRecycleBin: boolean;
Shareinfo: { Status: string; ShareTime: string | null } | null;
LastUploadTime?: string;
UserID?: number;
DeviceID?: number;
}FileListResult
interface FileListResult {
files: DegooFile[];
nextToken: string | null; // null = last page
}UploadResult
interface UploadResult {
name: string;
pathId: string;
alreadyExists: boolean;
file?: DegooFile;
}DownloadResult
interface DownloadResult {
path: string; // local path where the file was saved
size: number; // bytes written
}ByteRange
interface ByteRange {
start: number; // inclusive, ≥ 0
end?: number; // inclusive; omit for "to end of file"
}DownloadStreamOptions
interface DownloadStreamOptions {
range?: ByteRange;
signal?: AbortSignal;
timeoutMs?: number; // default 60_000
retries?: number; // default 3 (pre-body)
}DownloadStreamResult
interface DownloadStreamResult {
stream: NodeJS.ReadableStream;
size?: number;
contentRange?: string;
statusCode: number;
url: string;
}UserProfile
interface UserProfile {
ID: string;
FirstName: string;
LastName: string;
Email: string;
AvatarURL: string | null;
CountryCode: string;
LanguageCode: string;
Phone: string | null;
AccountType: number; // 1 = Free, 2 = Pro
UsedQuota: number;
TotalQuota: number;
OAuth2Provider: string | null;
GPMigrationStatus: number | null;
}Known Limitations
delete() / restore() — "Got empty result!"
Degoo's setDeleteFile5 mutation does not work for files uploaded programmatically via the API on certain account types. The operation returns "Got empty result!" regardless of input format.
Workaround: use hide() / unhide() to remove files from view without deleting them. Files uploaded through the Degoo web or mobile app can be deleted normally.
try {
await client.delete([fileId]);
} catch (err) {
if (err instanceof DegooError && err.message === 'Got empty result!') {
const { MetadataID } = await client.getFile(fileId);
if (MetadataID) await client.hide(MetadataID);
}
}Virtual root (pathId = '0') is read-only
The account root ID '0' is a virtual aggregation node. Passing it as the destination for upload(), createDirectory(), or move() returns "Error creating entries!" or "Invalid input!". Always use a real folder ID — for example, the "My Drive" folder returned in the root listing.
createDirectory() may return null
The folder-creation mutation does not return the new folder. The SDK resolves it via a search immediately after creation, but search-index latency can cause null to be returned even on success.
listFiles() — URL may be empty
getFileChildren5 does not always populate presigned download URLs in listing responses. Use getFile(id) (or getFileInfo(id)) to reliably obtain a download URL.
listByCategory() — ascending order only
Degoo's getCategoryContent rejects Order: 2 with "Invalid input!". Only ascending order is supported.
Architecture
The SDK is structured around four focused services composed behind a single DegooClient facade:
DegooClient (facade)
├── AuthService — login, logout, session restore, token refresh
├── FileService — listing, search, metadata, file mutations, sharing
├── UploadService — checksum, S3 presigned POST, metadata registration
└── DownloadService — URL resolution, streaming download (range, abort, retry)Each service depends on an interface (not a concrete class), so they can be replaced in tests without touching the facade.
The download layer in particular is composed from small, individually testable pieces:
download() / downloadFileStream()
│
▼
resolveDownloadUrl → openHttpStream → requestWithRedirects
(DRY URL lookup) (retry + backoff) (single HTTP attempt,
redirect, abort, timeout)Pure helpers (isRedirect, isRetriableStatus, isPrivateRedirectTarget, resolveSafeDestPath, buildRangeHeaders, assertNonEmptyString, assertValidRange, normalizeError) live at module scope so they can be reasoned about and tested in isolation.
Author
Duy Khanh — @khanhnd157
Repository: https://github.com/khanhnd157/degoo-api
Issues, pull requests, and security advisories are welcome on GitHub.
License
Released under the MIT License.
