Skip to content

Instantly share code, notes, and snippets.

@Xetera
Created November 14, 2025 15:43
Show Gist options
  • Select an option

  • Save Xetera/645481c34a3d054c4231ccb516e51e6e to your computer and use it in GitHub Desktop.

Select an option

Save Xetera/645481c34a3d054c4231ccb516e51e6e to your computer and use it in GitHub Desktop.
Gyazo's PRO data export does NOT properly include the created at field for the images you download. This script restores that data back into the images given they're all in the same folder and that you have an api key.
// Yes this is vibe coded. It worked fine for me
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"
)
// GyazoImage represents an image object from the API
type GyazoImage struct {
ImageID string `json:"image_id"`
CreatedAt string `json:"created_at"`
}
// Checkpoint represents saved progress
type Checkpoint struct {
Page int `json:"page"`
TotalProcessed int `json:"total_processed"`
TotalUpdated int `json:"total_updated"`
Timestamp time.Time `json:"timestamp"`
}
const checkpointFile = ".gyazo_checkpoint.json"
// getGyazoImages fetches images from Gyazo API for a given page
func getGyazoImages(accessToken string, page int) ([]GyazoImage, error) {
url := fmt.Sprintf("https://api.gyazo.com/api/images?access_token=%s&page=%d", accessToken, page)
resp, err := http.Get(url)
if err != nil {
return nil, fmt.Errorf("error fetching page %d: %w", page, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API returned status %d for page %d", resp.StatusCode, page)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("error reading response: %w", err)
}
var images []GyazoImage
if err := json.Unmarshal(body, &images); err != nil {
return nil, fmt.Errorf("error parsing JSON: %w", err)
}
return images, nil
}
// findFileByImageID finds a file matching the image_id (without extension)
func findFileByImageID(imageID, searchDir string) (string, error) {
pattern := filepath.Join(searchDir, imageID+".*")
matches, err := filepath.Glob(pattern)
if err != nil {
return "", err
}
if len(matches) > 0 {
return matches[0], nil
}
return "", nil
}
// updateFileTimestamp updates the access and modification time of a file
func updateFileTimestamp(filepath, isoTimestamp string) error {
// Try multiple timestamp formats
formats := []string{
time.RFC3339, // 2006-01-02T15:04:05Z07:00
"2006-01-02T15:04:05.999Z07:00", // With milliseconds
"2006-01-02T15:04:05-0700", // Without colon in timezone (e.g., +0000)
"2006-01-02T15:04:05.999-0700", // With milliseconds, no colon in timezone
}
var t time.Time
var err error
for _, format := range formats {
t, err = time.Parse(format, isoTimestamp)
if err == nil {
break
}
}
if err != nil {
return fmt.Errorf("error parsing timestamp '%s': %w", isoTimestamp, err)
}
// Update both access time and modification time
if err := os.Chtimes(filepath, t, t); err != nil {
return fmt.Errorf("error updating timestamps: %w", err)
}
return nil
}
// saveCheckpoint saves the current progress to a checkpoint file
func saveCheckpoint(page, totalProcessed, totalUpdated int) error {
checkpoint := Checkpoint{
Page: page,
TotalProcessed: totalProcessed,
TotalUpdated: totalUpdated,
Timestamp: time.Now(),
}
data, err := json.MarshalIndent(checkpoint, "", " ")
if err != nil {
return err
}
return os.WriteFile(checkpointFile, data, 0644)
}
// loadCheckpoint loads progress from a checkpoint file
func loadCheckpoint() (*Checkpoint, error) {
data, err := os.ReadFile(checkpointFile)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}
var checkpoint Checkpoint
if err := json.Unmarshal(data, &checkpoint); err != nil {
return nil, err
}
return &checkpoint, nil
}
// promptUser prompts the user for input
func promptUser(prompt string) string {
fmt.Print(prompt)
var input string
fmt.Scanln(&input)
return strings.TrimSpace(input)
}
func main() {
// Get access token from user
fmt.Print("Enter your Gyazo API access token: ")
var accessToken string
fmt.Scanln(&accessToken)
accessToken = strings.TrimSpace(accessToken)
if accessToken == "" {
fmt.Println("Error: Access token is required")
return
}
// Optional: specify a different search directory
fmt.Print("Enter directory to search for files (press Enter for current directory): ")
var searchDir string
fmt.Scanln(&searchDir)
searchDir = strings.TrimSpace(searchDir)
if searchDir == "" {
searchDir = "."
}
absPath, err := filepath.Abs(searchDir)
if err != nil {
fmt.Printf("Error resolving directory path: %v\n", err)
return
}
fmt.Printf("\nSearching for files in: %s\n\n", absPath)
// Check for existing checkpoint
checkpoint, err := loadCheckpoint()
if err != nil {
fmt.Printf("Warning: Could not load checkpoint: %v\n", err)
}
page := 1
totalProcessed := 0
totalUpdated := 0
if checkpoint != nil {
fmt.Printf("Found checkpoint from %s\n", checkpoint.Timestamp.Format(time.RFC3339))
fmt.Printf(" Last processed page: %d\n", checkpoint.Page)
fmt.Printf(" Total processed: %d\n", checkpoint.TotalProcessed)
fmt.Printf(" Total updated: %d\n", checkpoint.TotalUpdated)
resume := promptUser("\nResume from checkpoint? (y/n): ")
if strings.ToLower(resume) == "y" {
page = checkpoint.Page
totalProcessed = checkpoint.TotalProcessed
totalUpdated = checkpoint.TotalUpdated
fmt.Printf("\nResuming from page %d...\n\n", page)
} else {
fmt.Println("\nStarting fresh...\n")
}
}
// Process pages
for {
fmt.Printf("Fetching page %d...\n", page)
images, err := getGyazoImages(accessToken, page)
if err != nil {
fmt.Printf("Error: %v\n", err)
break
}
// Stop if no images returned or empty array
if len(images) == 0 {
fmt.Printf("No more images found at page %d\n", page)
break
}
fmt.Printf("Processing %d images from page %d...\n", len(images), page)
for _, image := range images {
if image.ImageID == "" || image.CreatedAt == "" {
fmt.Printf("Skipping image with missing data: %+v\n", image)
continue
}
totalProcessed++
// Find matching file
filepath, err := findFileByImageID(image.ImageID, searchDir)
if err != nil {
fmt.Printf("Error searching for file %s: %v\n", image.ImageID, err)
continue
}
if filepath != "" {
if err := updateFileTimestamp(filepath, image.CreatedAt); err != nil {
fmt.Printf("✗ Failed to update: %s (%v)\n", filepath, err)
} else {
fmt.Printf("✓ Updated: %s -> %s\n", filepath, image.CreatedAt)
totalUpdated++
}
} else {
fmt.Printf(" File not found for image_id: %s\n", image.ImageID)
}
}
// Save checkpoint after processing each page
page++
if err := saveCheckpoint(page, totalProcessed, totalUpdated); err != nil {
fmt.Printf("Warning: Could not save checkpoint: %v\n", err)
}
// Small delay to be nice to the API
time.Sleep(500 * time.Millisecond)
}
// Delete checkpoint file when complete
if err := os.Remove(checkpointFile); err != nil && !os.IsNotExist(err) {
fmt.Printf("Warning: Could not remove checkpoint file: %v\n", err)
} else {
fmt.Println("\nCheckpoint file removed (processing complete)")
}
fmt.Println("\n" + strings.Repeat("=", 50))
fmt.Println("Summary:")
fmt.Printf(" Total images processed: %d\n", totalProcessed)
fmt.Printf(" Files updated: %d\n", totalUpdated)
fmt.Printf(" Files not found: %d\n", totalProcessed-totalUpdated)
fmt.Println(strings.Repeat("=", 50))
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment