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
36 changes: 36 additions & 0 deletions delete_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package main

import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"
)

func TestDeleteShare(t *testing.T) {
var got struct{ method, path, auth string }
status, body := http.StatusNoContent, ""
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
got.method, got.path, got.auth = r.Method, r.URL.Path, r.Header.Get("Authorization")
w.WriteHeader(status)
_, _ = w.Write([]byte(body))
}))
defer srv.Close()

if err := deleteShare(context.Background(), srv.Client(), srv.URL, "01HXYZ", "tok"); err != nil {
t.Fatalf("204: %v", err)
}
if got.method != http.MethodDelete || got.path != "/api/pins/01HXYZ" || got.auth != "Bearer tok" {
t.Fatalf("sent %+v", got)
}

status = http.StatusNotFound
if err := deleteShare(context.Background(), srv.Client(), srv.URL, "01HXYZ", "tok"); err == nil || !strings.Contains(err.Error(), "not found") {
t.Fatalf("404: %v", err)
}
status, body = http.StatusForbidden, "only the person who shared it can delete it"
if err := deleteShare(context.Background(), srv.Client(), srv.URL, "01HXYZ", "tok"); err == nil || !strings.Contains(err.Error(), "http 403: only the person") {
t.Fatalf("403: %v", err)
}
}
67 changes: 67 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ func main() {
os.Exit(runPublish(os.Args[2:]))
case "unpublish":
os.Exit(runUnpublish(os.Args[2:]))
case "delete":
os.Exit(runDelete(os.Args[2:]))
case "components":
os.Exit(runComponents(os.Args[2:]))
case "whoami":
Expand Down Expand Up @@ -90,6 +92,7 @@ Usage:
capability link. --ttl <dur> sets the lifetime
(default 7d, max 30d). Prints the public URL.
pin unpublish <token|url> Revoke a public link before it expires.
pin delete <id-or-url> Delete a share you made, every form of it.
pin components List MDX components, grouped by category.
pin components get <Name> Show one component's props + example.
pin components dump Print every component's full detail.
Expand Down Expand Up @@ -702,6 +705,70 @@ func runUnpublish(args []string) int {
return 0
}

// runDelete deletes a share the signed-in person made. Only its owner can:
// the server refuses anyone else (403), and a share made before pin recorded
// owners cannot be deleted this way.
func runDelete(args []string) int {
if len(args) != 1 {
fmt.Fprintln(os.Stderr, "usage: pin delete <id-or-url>")
return 2
}
id, hostFromURL := parseShareRef(args[0])
if id == "" {
fmt.Fprintf(os.Stderr, "pin delete: not a share id or URL: %q\n", args[0])
return 2
}

ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()

c, err := loadCreds()
if err != nil {
fmt.Fprintf(os.Stderr, "pin delete: not logged in. Run `pin login`.\n")
return 1
}
c, err = ensureFreshAccess(ctx, c)
if err != nil {
fmt.Fprintf(os.Stderr, "pin delete: refresh: %v\n", err)
return 1
}

base := hostFromURL
if base == "" {
base = host()
}
if err := deleteShare(ctx, http.DefaultClient, base, id, c.AccessToken); err != nil {
fmt.Fprintf(os.Stderr, "pin delete: %v\n", err)
return 1
}
fmt.Fprintf(os.Stderr, "deleted %s\n", id)
return 0
}

// deleteShare sends DELETE /api/pins/{id}; nil only on 204.
func deleteShare(ctx context.Context, client *http.Client, base, id, accessToken string) error {
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, base+"/api/pins/"+url.PathEscape(id), nil)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("X-Agent", agent())
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusNoContent:
return nil
case http.StatusNotFound:
return fmt.Errorf("not found: %s", id)
default:
rbody, _ := io.ReadAll(resp.Body)
return fmt.Errorf("http %d: %s", resp.StatusCode, strings.TrimSpace(string(rbody)))
}
}

// parsePublicRef extracts the capability token from either a bare token
// or a full public URL like
// "https://pin.bitcomplete.dev/public/p/{id}?token=…". Returns (token,
Expand Down
Loading