Skip to content
Merged
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,8 +223,8 @@ api:

| Feature | Status |
|---|---|
| Get data | 🔜 |
| Get quota / capabilities | 🔜 |
| Get data | |
| Get quota | ✅ |

### Organization

Expand Down
20 changes: 4 additions & 16 deletions cmd/transfer.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/retyc/retyc-cli/internal/config"
"github.com/retyc/retyc-cli/internal/crypto"
"github.com/retyc/retyc-cli/internal/keyring"
"github.com/retyc/retyc-cli/internal/ui"
"github.com/schollz/progressbar/v3"
"github.com/spf13/cobra"
"golang.org/x/oauth2"
Expand Down Expand Up @@ -187,7 +188,7 @@ var transferInfoCmd = &cobra.Command{
if err != nil {
name = "(encrypted)"
}
fmt.Fprintf(w, " %s\t%s\n", name, formatSize(f.OriginalSize))
fmt.Fprintf(w, " %s\t%s\n", name, ui.FormatSize(f.OriginalSize))
}
_ = w.Flush()

Expand Down Expand Up @@ -341,14 +342,14 @@ func confirmFileList(names []string, sizes []int64, totalSize int64, extras []st
if len(runes) > lineWidth-10 {
name = string(runes[:lineWidth-13]) + "…"
}
fmt.Fprintf(os.Stderr, " %-*s %s\n", lineWidth-10, name, formatSize(sizes[i]))
fmt.Fprintf(os.Stderr, " %-*s %s\n", lineWidth-10, name, ui.FormatSize(sizes[i]))
}
fmt.Fprintf(os.Stderr, " %s\n", strings.Repeat("─", lineWidth))
noun := "file"
if len(names) > 1 {
noun = "files"
}
fmt.Fprintf(os.Stderr, " %-*s %s\n", lineWidth-10, fmt.Sprintf("%d %s", len(names), noun), formatSize(totalSize))
fmt.Fprintf(os.Stderr, " %-*s %s\n", lineWidth-10, fmt.Sprintf("%d %s", len(names), noun), ui.FormatSize(totalSize))
fmt.Fprintln(os.Stderr)
for _, extra := range extras {
fmt.Fprintln(os.Stderr, extra)
Expand Down Expand Up @@ -381,19 +382,6 @@ func ptrOr(s *string, fallback string) string {
return *s
}

// formatSize formats a byte count as a human-readable string (e.g. "1.4 MiB").
func formatSize(bytes int64) string {
if bytes < 1024 {
return fmt.Sprintf("%d B", bytes)
}
div, exp := int64(1024), 0
for n := bytes / 1024; n >= 1024; n /= 1024 {
div *= 1024
exp++
}

return fmt.Sprintf("%.1f %ciB", float64(bytes)/float64(div), "KMGTPE"[exp])
}

// uploadChunkSize is the size of each plaintext chunk before encryption.
const uploadChunkSize = 8 * 1024 * 1024 // 8 MB
Expand Down
91 changes: 91 additions & 0 deletions cmd/user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package cmd

import (
"fmt"

"github.com/retyc/retyc-cli/internal/ui"
"github.com/spf13/cobra"
)

var userCmd = &cobra.Command{
Use: "user",
Short: "Manage user account",
}

var userInfoCmd = &cobra.Command{
Use: "info",
Short: "Show current user information",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
_, client, err := newAPIClient(ctx)
if err != nil {
return err
}

u, err := client.GetMe(ctx)
if err != nil {
return fmt.Errorf("fetching user info: %w", err)
}

fullName := "-"
if u.FullName != nil && *u.FullName != "" {
fullName = *u.FullName
}

fmt.Printf("ID: %s\n", u.ID)
fmt.Printf("Email: %s\n", u.Email)
fmt.Printf("Full name: %s\n", fullName)
fmt.Printf("Role: %s\n", u.OrganizationRole)
fmt.Printf("Plan: %s\n", u.OrganizationPlanID)
fmt.Printf("Public key: %s\n", u.PublicKey)

return nil
},
}

var userQuotaCmd = &cobra.Command{
Use: "quota",
Short: "Show current user quota",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
_, client, err := newAPIClient(ctx)
if err != nil {
return err
}

q, err := client.GetQuota(ctx)
if err != nil {
return fmt.Errorf("fetching quota: %w", err)
}

readOnly := ""
if q.IsUploadReadOnly {
readOnly = " (read-only)"
}

fmt.Printf("Transfers: %d / %s\n", q.CountShare, formatCount(q.MaxCountShare))
fmt.Printf("Datarooms: %d / %s\n", q.CountDataroom, formatCount(q.MaxCountDataroom))
fmt.Printf("Storage: %s / %s%s\n",
ui.FormatSize(q.UsedStorage),
ui.FormatSize(q.MaxStorage),
readOnly,
)

return nil
},
}

// formatCount formats a nullable count limit: nil means unlimited.
func formatCount(n *int) string {
if n == nil {
return "unlimited"
}

return fmt.Sprintf("%d", *n)
}

func init() {
userCmd.AddCommand(userInfoCmd)
userCmd.AddCommand(userQuotaCmd)
rootCmd.AddCommand(userCmd)
}
46 changes: 46 additions & 0 deletions internal/api/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,52 @@ import (
"time"
)

// UserInfo holds the authenticated user's profile information as returned by GET /user/me.
type UserInfo struct {
ID string `json:"id"`
Email string `json:"email"`
FullName *string `json:"full_name"`
OrganizationRole string `json:"organization_role"`
OrganizationPlanID string `json:"organization_plan_id"`
PublicKey string `json:"public_key"`
}

// userMeResponse is the top-level wrapper returned by GET /user/me.
type userMeResponse struct {
User UserInfo `json:"user"`
}

// GetMe retrieves the authenticated user's profile.
func (c *Client) GetMe(ctx context.Context) (*UserInfo, error) {
var result userMeResponse
if err := c.Get(ctx, "/user/me", &result); err != nil {
return nil, err
}

return &result.User, nil
}

// UserQuota holds the authenticated user's quota information as returned by GET /user/quota.
type UserQuota struct {
CountShare int `json:"count_share"`
MaxCountShare *int `json:"max_count_share"`
CountDataroom int `json:"count_dataroom"`
MaxCountDataroom *int `json:"max_count_dataroom"`
UsedStorage int64 `json:"used_storage"`
MaxStorage int64 `json:"max_storage"`
IsUploadReadOnly bool `json:"is_upload_read_only"`
}

// GetQuota retrieves the authenticated user's quota.
func (c *Client) GetQuota(ctx context.Context) (*UserQuota, error) {
var result UserQuota
if err := c.Get(ctx, "/user/quota", &result); err != nil {
return nil, err
}

return &result, nil
}

// UserKey holds the user's active AGE encryption key pair as stored by the API.
// PrivateKeyEnc contains the AGE private key encrypted with the user's passphrase
// (AGE scrypt recipient). It must be decrypted locally before use.
Expand Down
17 changes: 17 additions & 0 deletions internal/ui/format.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package ui

import "fmt"

// FormatSize formats a byte count as a human-readable string (e.g. "1.4 MiB").
func FormatSize(bytes int64) string {
if bytes < 1024 {
return fmt.Sprintf("%d B", bytes)
}
div, exp := int64(1024), 0
for n := bytes / 1024; n >= 1024; n /= 1024 {
div *= 1024
exp++
}

return fmt.Sprintf("%.1f %ciB", float64(bytes)/float64(div), "KMGTPE"[exp])
}