├── .gitignore ├── go.mod ├── client ├── config.go ├── general.go ├── entitiy.go ├── media_test.go ├── general_test.go ├── config_test.go ├── media.go ├── contents_test.go └── contents.go ├── main.go ├── go.sum └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | backup/ 2 | config.json 3 | .DS_Store 4 | .env -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Sinhalite/microcms-backup-tool 2 | 3 | go 1.23.2 4 | 5 | require( 6 | github.com/tidwall/gjson v1.18.0 7 | github.com/joho/godotenv v1.5.1 8 | ) 9 | 10 | require ( 11 | github.com/tidwall/match v1.1.1 // indirect 12 | github.com/tidwall/pretty v1.2.1 // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /client/config.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | ) 7 | 8 | func (c *Client) LoadConfig(configPath string) error { 9 | // デフォルト値を設定 10 | c.Config.Contents.RequestUnit = 10 11 | 12 | f, err := os.Open(configPath) 13 | if err != nil { 14 | return err 15 | } 16 | defer f.Close() 17 | 18 | d := json.NewDecoder(f) 19 | d.DisallowUnknownFields() 20 | if err := d.Decode(c.Config); err != nil { 21 | return err 22 | } 23 | return nil 24 | } 25 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/Sinhalite/microcms-backup-tool/client" 7 | ) 8 | 9 | func main() { 10 | client := &client.Client{Config: &client.Config{}} 11 | err := client.LoadConfig("config.json") 12 | if err != nil { 13 | log.Fatal("正常にオプションをセットできませんでした") 14 | } 15 | 16 | baseDir, err := client.MakeBackupDir() 17 | if err != nil { 18 | log.Fatal("正常にバックアップディレクトリを作成できませんでした") 19 | } 20 | 21 | err = client.StartBackup(baseDir) 22 | if err != nil { 23 | log.Printf("バックアップに失敗しました: %v", err) 24 | log.Fatal("正常にバックアップを処理できませんでした") 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 2 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 3 | github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= 4 | github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 5 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 6 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 7 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 8 | github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= 9 | github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 10 | -------------------------------------------------------------------------------- /client/general.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "time" 8 | ) 9 | 10 | func (c Client) MakeBackupDir() (string, error) { 11 | // バックアップのディレクトリ作成 12 | t := time.Now() 13 | timeDir := t.Format("2006_01_02_15_04_05") 14 | baseDir := "backup/" + c.Config.ServiceID + "/" + timeDir + "/" 15 | 16 | err := os.MkdirAll(baseDir, os.ModePerm) 17 | if err != nil { 18 | return "", err 19 | } 20 | log.Println("バックアップディレクトリを作成しました") 21 | return baseDir, nil 22 | } 23 | 24 | func (c Client) StartBackup(baseDir string) error { 25 | log.Println("バックアップを開始します") 26 | 27 | switch c.Config.Target { 28 | case "all": 29 | err := c.BackupContents(baseDir) 30 | if err != nil { 31 | return err 32 | } 33 | err = c.BackupMedia(baseDir) 34 | if err != nil { 35 | return err 36 | } 37 | case "contents": 38 | err := c.BackupContents(baseDir) 39 | if err != nil { 40 | return err 41 | } 42 | case "media": 43 | err := c.BackupMedia(baseDir) 44 | if err != nil { 45 | return err 46 | } 47 | default: 48 | return fmt.Errorf("不明なターゲットが選択されました") 49 | } 50 | log.Println("正常にバックアップが終了しました") 51 | return nil 52 | } 53 | -------------------------------------------------------------------------------- /client/entitiy.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | type ContentsAPIResponse struct { 8 | Contents json.RawMessage `json:"contents"` 9 | TotalCount int `json:"totalCount"` 10 | Offset int `json:"offset"` 11 | Limit int `json:"limit"` 12 | } 13 | 14 | type ManagementAPIMediaResponse struct { 15 | Media []Media `json:"media"` 16 | TotalCount int `json:"totalCount"` 17 | Token string `json:"token"` 18 | } 19 | 20 | type Media struct { 21 | Id string `json:"id"` 22 | Url string `json:"url"` 23 | Width int `json:"width"` 24 | Height int `json:"height"` 25 | } 26 | 27 | // ContentsConfig はコンテンツバックアップの設定を保持する構造体 28 | type ContentsConfig struct { 29 | // 公開コンテンツを取得するためのAPIキー(classifyByStatusがfalseの場合はこれのみ必要) 30 | GetPublishContentsAPIKey string `json:"getPublishContentsAPIKey"` 31 | // 全ステータスのコンテンツを取得するためのAPIキー(classifyByStatusがtrueの場合に必要) 32 | GetAllStatusContentsAPIKey string `json:"getAllStatusContentsAPIKey"` 33 | // コンテンツのメタデータを取得するためのAPIキー(classifyByStatusがtrueの場合に必要) 34 | GetContentsMetaDataAPIKey string `json:"getContentsMetaDataAPIKey"` 35 | Endpoints []string `json:"endpoints"` 36 | RequestUnit int `json:"requestUnit"` 37 | ClassifyByStatus bool `json:"classifyByStatus"` 38 | // CSVファイルとして保存するかどうか 39 | SaveAsCSV bool `json:"saveAsCSV"` 40 | } 41 | 42 | // MediaConfig はメディアバックアップの設定を保持する構造体 43 | type MediaConfig struct { 44 | APIKey string `json:"apiKey"` 45 | } 46 | 47 | type Config struct { 48 | Target string `json:"target"` 49 | ServiceID string `json:"serviceId"` 50 | Contents ContentsConfig `json:"contents"` 51 | Media MediaConfig `json:"media"` 52 | } 53 | 54 | type Client struct { 55 | Config *Config 56 | } 57 | -------------------------------------------------------------------------------- /client/media_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | "time" 9 | 10 | "github.com/joho/godotenv" 11 | ) 12 | 13 | func init() { 14 | // Get the current working directory 15 | wd, err := os.Getwd() 16 | if err != nil { 17 | panic("Error getting working directory") 18 | } 19 | 20 | // Get the project root directory (where go.mod is located) 21 | projectRoot := filepath.Dir(wd) 22 | 23 | // Load .env file from the project root 24 | envPath := filepath.Join(projectRoot, ".env") 25 | if err := godotenv.Load(envPath); err != nil { 26 | panic("Error loading .env file: " + envPath) 27 | } 28 | } 29 | 30 | func TestBackupMedia(t *testing.T) { 31 | type args struct { 32 | config *Config 33 | baseDir string 34 | } 35 | 36 | mediaAPIKey := os.Getenv("MEDIA_API_KEY") 37 | if mediaAPIKey == "" { 38 | t.Skip("Media API key not set in environment variable") 39 | } 40 | 41 | tests := []struct { 42 | name string 43 | args args 44 | want bool 45 | }{ 46 | { 47 | name: "api key incorrect", 48 | args: args{ 49 | config: &Config{ 50 | Target: "media", 51 | ServiceID: "backup-test", 52 | Media: MediaConfig{ 53 | APIKey: "incorrectkey", 54 | }, 55 | }, 56 | baseDir: "../backup/test/", 57 | }, 58 | want: false, 59 | }, 60 | { 61 | name: "normal", 62 | args: args{ 63 | config: &Config{ 64 | Target: "media", 65 | ServiceID: "backup-test", 66 | Media: MediaConfig{ 67 | APIKey: mediaAPIKey, 68 | }, 69 | }, 70 | baseDir: "../backup/test/", 71 | }, 72 | want: true, 73 | }, 74 | } 75 | for _, tt := range tests { 76 | t.Run(tt.name, func(t *testing.T) { 77 | time.Sleep(5 * time.Second) 78 | 79 | client := Client{} 80 | client.Config = tt.args.config 81 | 82 | err := client.BackupMedia(tt.args.baseDir) 83 | if err != nil { 84 | fmt.Println(err) 85 | } 86 | got := err == nil 87 | if got != tt.want { 88 | t.Errorf("backupMedia() = %v, want %v", got, tt.want) 89 | } 90 | }) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # microcms-backup-tool 2 | 3 | Screenshot 4 | 5 | # 概要 6 | 7 | microCMS で管理しているコンテンツとメディア(画像・ファイル)を取得し、保存するツールです。 8 | 9 | # 注意事項 10 | 11 | - 非公式ツールです。利用にあたっては、自己責任にてお願いいたします。 12 | - 一部は動作保証のないベータ版の機能であるマネジメントAPI (https://document.microcms.io/management-api/get-media) を利用しています。 13 | - 利用するAPIキーには、あらかじめ適切な権限付与が必要です。詳しくは API キーのドキュメント (https://document.microcms.io/content-api/x-microcms-api-key) を確認してください。 14 | - APIキーの秘匿等の考慮はされていないため、取り扱いにはご注意ください。 15 | 16 | # 利用方法 17 | 18 | 1. ルートディレクトリに、`config.json`を作成し、必要情報を設定してください。 19 | 2. バックアップ対象のサービスにおいて、適切なAPIキーの権限付与を行います。 20 | 3. ルートディレクトリにて、`go run .`を実行します。 21 | 4. `backup`フォルダの中に、指定したデータのバックアップファイルが保存されます。 22 | 23 | # 設定ファイル 24 | 25 | `config.json` 26 | ```json 27 | { 28 | "target": "all", 29 | "serviceId": "xxxxxxxxxx", 30 | "contents": { 31 | "getPublishContentsAPIKey": "xxxxxxxxxxxxxxxxxxxxxxxx", 32 | "getAllStatusContentsAPIKey": "xxxxxxxxxxxxxxxxxxxxxxxx", 33 | "getContentsMetaDataAPIKey": "xxxxxxxxxxxxxxxxxxxxxxxx", 34 | "endpoints": ["hoge", "fuga"], 35 | "requestUnit": 100, 36 | "classifyByStatus": true, 37 | "saveAsCSV": false 38 | }, 39 | "media": { 40 | "apiKey": "xxxxxxxxxxxxxxxxxxxxxxxx" 41 | } 42 | } 43 | ``` 44 | 45 | 設定されたサービスに対してバックアップを実施します。 46 | 47 | ## target 48 | `target`は、以下の 3 項目より選択してください。 49 | 50 | - `all` : コンテンツとメディア 51 | - `contents` : コンテンツのみ 52 | - `media` : メディアのみ 53 | 54 | ## APIキー 55 | 56 | `contents.getPublishContentsAPIKey` 57 | - 公開中のみのコンテンツのGET権限を付与してください 58 | - 公開中コンテンツの取得に使用 59 | 60 | `contents.getAllStatusContentsAPIKey` 61 | - 公開中、下書き、公開終了のコンテンツのGET権限を付与してください 62 | - 全ステータスのコンテンツ取得に使用 63 | 64 | `contents.getContentsMetaDataAPIKey` 65 | - コンテンツのメタデータのGET権限を付与してください 66 | - コンテンツのステータス情報取得に使用 67 | 68 | `media.getMediaAPIKey` 69 | - メディアのGET権限を付与してください 70 | - メディアファイルの取得に使用 71 | 72 | ## コンテンツの保存形式 73 | 74 | ### 1. ステータス別分類あり(`classifyByStatus: true`) 75 | 76 | コンテンツはステータスごとに分類されて保存されます。 77 | 78 | #### JSON形式(`saveAsCSV: false`) 79 | - コンテンツは個別のJSONファイルとして保存されます 80 | 81 | #### CSV形式(`saveAsCSV: true`) 82 | - コンテンツは1つのCSVファイルとして保存されます 83 | - ネストされたJSONオブジェクトや配列は文字列として保存されます 84 | - ファイル名は`contents.csv`となります 85 | 86 | ### 2. ステータス別分類なし(`classifyByStatus: false`) 87 | 88 | コンテンツは1つのファイルとして保存されます。 89 | 90 | #### JSON形式(`saveAsCSV: false`) 91 | - コンテンツは個別のJSONファイルとして保存されます 92 | 93 | #### CSV形式(`saveAsCSV: true`) 94 | - コンテンツは1つのCSVファイルとして保存されます 95 | - ネストされたJSONオブジェクトや配列は文字列として保存されます 96 | - ファイル名は`contents.csv`となります 97 | -------------------------------------------------------------------------------- /client/general_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | "time" 8 | 9 | "github.com/joho/godotenv" 10 | ) 11 | 12 | func init() { 13 | // Get the current working directory 14 | wd, err := os.Getwd() 15 | if err != nil { 16 | panic("Error getting working directory") 17 | } 18 | 19 | // Get the project root directory (where go.mod is located) 20 | projectRoot := filepath.Dir(wd) 21 | 22 | // Load .env file from the project root 23 | envPath := filepath.Join(projectRoot, ".env") 24 | if err := godotenv.Load(envPath); err != nil { 25 | panic("Error loading .env file: " + envPath) 26 | } 27 | } 28 | 29 | func TestBackupAllTargets(t *testing.T) { 30 | type args struct { 31 | config *Config 32 | baseDir string 33 | } 34 | 35 | publishAPIKey := os.Getenv("PUBLISH_API_KEY") 36 | allStatusAPIKey := os.Getenv("ALL_STATUS_API_KEY") 37 | metaDataAPIKey := os.Getenv("META_DATA_API_KEY") 38 | mediaAPIKey := os.Getenv("MEDIA_API_KEY") 39 | 40 | if publishAPIKey == "" || allStatusAPIKey == "" || metaDataAPIKey == "" || mediaAPIKey == "" { 41 | t.Skip("API keys not set in environment variables") 42 | } 43 | 44 | tests := []struct { 45 | name string 46 | args args 47 | want bool 48 | }{ 49 | { 50 | name: "backup contents only", 51 | args: args{ 52 | config: &Config{ 53 | Target: "contents", 54 | ServiceID: "backup-test", 55 | Contents: ContentsConfig{ 56 | GetPublishContentsAPIKey: publishAPIKey, 57 | GetAllStatusContentsAPIKey: allStatusAPIKey, 58 | GetContentsMetaDataAPIKey: metaDataAPIKey, 59 | Endpoints: []string{"test", "test2"}, 60 | RequestUnit: 10, 61 | ClassifyByStatus: true, 62 | SaveAsCSV: true, 63 | }, 64 | }, 65 | baseDir: "../backup/test/", 66 | }, 67 | want: true, 68 | }, 69 | { 70 | name: "backup media only", 71 | args: args{ 72 | config: &Config{ 73 | Target: "media", 74 | ServiceID: "backup-test", 75 | Media: MediaConfig{ 76 | APIKey: mediaAPIKey, 77 | }, 78 | }, 79 | baseDir: "../backup/test/", 80 | }, 81 | want: true, 82 | }, 83 | { 84 | name: "backup all targets", 85 | args: args{ 86 | config: &Config{ 87 | Target: "all", 88 | ServiceID: "backup-test", 89 | Contents: ContentsConfig{ 90 | GetPublishContentsAPIKey: publishAPIKey, 91 | GetAllStatusContentsAPIKey: allStatusAPIKey, 92 | GetContentsMetaDataAPIKey: metaDataAPIKey, 93 | Endpoints: []string{"test", "test2"}, 94 | RequestUnit: 10, 95 | ClassifyByStatus: true, 96 | SaveAsCSV: true, 97 | }, 98 | Media: MediaConfig{ 99 | APIKey: mediaAPIKey, 100 | }, 101 | }, 102 | baseDir: "../backup/test/", 103 | }, 104 | want: true, 105 | }, 106 | } 107 | 108 | for _, tt := range tests { 109 | t.Run(tt.name, func(t *testing.T) { 110 | time.Sleep(5 * time.Second) 111 | 112 | client := &Client{} 113 | client.Config = tt.args.config 114 | 115 | err := client.StartBackup(tt.args.baseDir) 116 | got := err == nil 117 | if got != tt.want { 118 | t.Errorf("StartBackup() = %v, want %v", got, tt.want) 119 | } 120 | }) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /client/config_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestLoadConfig(t *testing.T) { 9 | // テスト用の一時ファイルを作成 10 | tmpFile := "test_config.json" 11 | defer os.Remove(tmpFile) 12 | 13 | tests := []struct { 14 | name string 15 | content string 16 | filePath string 17 | wantErr bool 18 | }{ 19 | { 20 | name: "正常系: 正しい形式のJSON", 21 | content: `{ 22 | "target": "contents", 23 | "serviceId": "test-service", 24 | "contents": { 25 | "getPublishContentsAPIKey": "test-key", 26 | "endpoints": ["test"], 27 | "requestUnit": 20, 28 | "classifyByStatus": false, 29 | "saveAsCSV": false 30 | } 31 | }`, 32 | filePath: tmpFile, 33 | wantErr: false, 34 | }, 35 | { 36 | name: "正常系: デフォルト値の確認", 37 | content: `{ 38 | "target": "contents", 39 | "serviceId": "test-service", 40 | "contents": { 41 | "getPublishContentsAPIKey": "test-key", 42 | "endpoints": ["test"], 43 | "classifyByStatus": false, 44 | "saveAsCSV": false 45 | } 46 | }`, 47 | filePath: tmpFile, 48 | wantErr: false, 49 | }, 50 | { 51 | name: "異常系: 存在しないファイル", 52 | content: "", 53 | filePath: "non_existent_config.json", 54 | wantErr: true, 55 | }, 56 | { 57 | name: "異常系: 不正なJSON形式", 58 | content: `{invalid json}`, 59 | filePath: tmpFile, 60 | wantErr: true, 61 | }, 62 | { 63 | name: "異常系: 未知のフィールド", 64 | content: `{ 65 | "target": "contents", 66 | "serviceId": "test-service", 67 | "contents": { 68 | "getPublishContentsAPIKey": "test-key", 69 | "endpoints": ["test"], 70 | "unknownField": "value" 71 | } 72 | }`, 73 | filePath: tmpFile, 74 | wantErr: true, 75 | }, 76 | } 77 | 78 | for _, tt := range tests { 79 | t.Run(tt.name, func(t *testing.T) { 80 | // テストファイルの準備 81 | if tt.content != "" { 82 | err := os.WriteFile(tt.filePath, []byte(tt.content), 0644) 83 | if err != nil { 84 | t.Fatalf("テストファイルの作成に失敗: %v", err) 85 | } 86 | } 87 | 88 | // テスト対象のクライアントを作成 89 | client := &Client{ 90 | Config: &Config{}, 91 | } 92 | 93 | // LoadConfigの実行 94 | err := client.LoadConfig(tt.filePath) 95 | 96 | // エラーチェック 97 | if (err != nil) != tt.wantErr { 98 | t.Errorf("LoadConfig() error = %v, wantErr %v", err, tt.wantErr) 99 | } 100 | 101 | // 正常系の場合、設定値の検証 102 | if !tt.wantErr { 103 | // デフォルト値の確認 104 | if tt.name == "正常系: デフォルト値の確認" { 105 | if client.Config.Contents.RequestUnit != 10 { 106 | t.Errorf("RequestUnit = %v, want %v", client.Config.Contents.RequestUnit, 10) 107 | } 108 | } 109 | 110 | // 基本設定の確認 111 | if client.Config.Target != "contents" { 112 | t.Errorf("Target = %v, want %v", client.Config.Target, "contents") 113 | } 114 | if client.Config.ServiceID != "test-service" { 115 | t.Errorf("ServiceID = %v, want %v", client.Config.ServiceID, "test-service") 116 | } 117 | 118 | // Contents設定の確認 119 | if client.Config.Contents.GetPublishContentsAPIKey != "test-key" { 120 | t.Errorf("GetPublishContentsAPIKey = %v, want %v", client.Config.Contents.GetPublishContentsAPIKey, "test-key") 121 | } 122 | if len(client.Config.Contents.Endpoints) != 1 || client.Config.Contents.Endpoints[0] != "test" { 123 | t.Errorf("Endpoints = %v, want %v", client.Config.Contents.Endpoints, []string{"test"}) 124 | } 125 | } 126 | }) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /client/media.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net/http" 9 | "net/url" 10 | "os" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | func (c Client) BackupMedia(baseDir string) error { 16 | log.Println("メディアのバックアップを開始します") 17 | const requestUnit = 100 18 | totalCount, err := c.getTotalCount() 19 | if err != nil { 20 | return fmt.Errorf("合計件数の取得でエラーが発生しました: %w", err) 21 | } 22 | requiredRequestCount := (totalCount/requestUnit + 1) 23 | 24 | mediaAry, err := c.getAllMedia(requiredRequestCount, requestUnit) 25 | if err != nil { 26 | return fmt.Errorf("メディア一覧の取得でエラーが発生しました: %w", err) 27 | } 28 | err = c.saveMedia(mediaAry, totalCount, baseDir) 29 | if err != nil { 30 | return fmt.Errorf("メディアの保存でエラーが発生しました: %w", err) 31 | } 32 | return nil 33 | } 34 | 35 | func (c Client) getTotalCount() (int, error) { 36 | url := fmt.Sprintf("https://%s.microcms-management.io/api/v2/media?limit=0", c.Config.ServiceID) 37 | req, _ := http.NewRequest("GET", url, nil) 38 | req.Header.Set("X-MICROCMS-API-KEY", c.Config.Media.APIKey) 39 | 40 | client := new(http.Client) 41 | resp, err := client.Do(req) 42 | 43 | if err != nil { 44 | return 0, err 45 | } 46 | 47 | if resp.StatusCode != http.StatusOK { 48 | return 0, fmt.Errorf("ステータスコード:%d 正常にレスポンスを取得できませんでした", resp.StatusCode) 49 | } 50 | defer resp.Body.Close() 51 | 52 | body, err := io.ReadAll(resp.Body) 53 | if err != nil { 54 | return 0, err 55 | } 56 | 57 | response := &ManagementAPIMediaResponse{} 58 | err = json.Unmarshal(body, response) 59 | if err != nil { 60 | return 0, err 61 | } 62 | 63 | return response.TotalCount, err 64 | } 65 | 66 | func (c Client) getAllMedia(requiredRequestCount int, requestUnit int) ([]Media, error) { 67 | var ary []Media 68 | var token string 69 | 70 | for i := 0; i < requiredRequestCount; i++ { 71 | // 1秒のディレイを追加 72 | if i > 0 { 73 | time.Sleep(3 * time.Second) 74 | } 75 | 76 | client := new(http.Client) 77 | req, _ := http.NewRequest( 78 | "GET", 79 | fmt.Sprintf("https://%s.microcms-management.io/api/v2/media?limit=%d&token=%s", c.Config.ServiceID, requestUnit, token), 80 | nil, 81 | ) 82 | req.Header.Set("X-MICROCMS-API-KEY", c.Config.Media.APIKey) 83 | resp, err := client.Do(req) 84 | if err != nil { 85 | return nil, err 86 | } 87 | if resp.StatusCode != http.StatusOK { 88 | return nil, fmt.Errorf("ステータスコード:%d 正常にレスポンスを取得できませんでした", resp.StatusCode) 89 | } 90 | defer resp.Body.Close() 91 | 92 | body, err := io.ReadAll(resp.Body) 93 | if err != nil { 94 | return nil, err 95 | } 96 | 97 | response := &ManagementAPIMediaResponse{} 98 | err = json.Unmarshal(body, response) 99 | if err != nil { 100 | return nil, err 101 | } 102 | 103 | ary = append(ary, response.Media...) 104 | token = response.Token 105 | } 106 | 107 | return ary, nil 108 | } 109 | 110 | func (c Client) saveMedia(medias []Media, totalCount int, baseDir string) error { 111 | for i, media := range medias { 112 | // 進捗状況の表示 113 | fmt.Printf("[%d / %d] %s\n", i+1, totalCount, media.Url) 114 | 115 | client := new(http.Client) 116 | req, _ := http.NewRequest("GET", media.Url, nil) 117 | req.Header.Set("X-MICROCMS-API-KEY", c.Config.Media.APIKey) 118 | 119 | resp, err := client.Do(req) 120 | if err != nil { 121 | return err 122 | } 123 | if resp.StatusCode != http.StatusOK { 124 | return fmt.Errorf("ステータスコード:%d 正常にレスポンスを取得できませんでした", resp.StatusCode) 125 | } 126 | defer resp.Body.Close() 127 | 128 | ary := strings.Split(media.Url, "/") 129 | fileName := ary[len(ary)-1] 130 | fileName, err = url.QueryUnescape(fileName) 131 | if err != nil { 132 | return err 133 | } 134 | fileDirectory := ary[len(ary)-2] 135 | 136 | // ファイルごとのディレクトリを作成する 137 | // (同じファイル名でアップロード可能なため、一意となるようなパスが付与されている) 138 | err = os.MkdirAll(baseDir+"media/"+fileDirectory, os.ModePerm) 139 | if err != nil { 140 | return err 141 | } 142 | 143 | file, err := os.Create(baseDir + "media/" + fileDirectory + "/" + fileName) 144 | if err != nil { 145 | return err 146 | 147 | } 148 | defer file.Close() 149 | 150 | _, err = io.Copy(file, resp.Body) 151 | if err != nil { 152 | return err 153 | } 154 | } 155 | return nil 156 | } 157 | -------------------------------------------------------------------------------- /client/contents_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | "time" 8 | 9 | "github.com/joho/godotenv" 10 | ) 11 | 12 | func init() { 13 | // Get the current working directory 14 | wd, err := os.Getwd() 15 | if err != nil { 16 | panic("Error getting working directory") 17 | } 18 | 19 | // Get the project root directory (where go.mod is located) 20 | projectRoot := filepath.Dir(wd) 21 | 22 | // Load .env file from the project root 23 | envPath := filepath.Join(projectRoot, ".env") 24 | if err := godotenv.Load(envPath); err != nil { 25 | panic("Error loading .env file: " + envPath) 26 | } 27 | } 28 | 29 | func TestBackupContents(t *testing.T) { 30 | type args struct { 31 | config *Config 32 | baseDir string 33 | } 34 | 35 | publishAPIKey := os.Getenv("PUBLISH_API_KEY") 36 | allStatusAPIKey := os.Getenv("ALL_STATUS_API_KEY") 37 | metaDataAPIKey := os.Getenv("META_DATA_API_KEY") 38 | 39 | if publishAPIKey == "" || allStatusAPIKey == "" || metaDataAPIKey == "" { 40 | t.Skip("API keys not set in environment variables") 41 | } 42 | 43 | tests := []struct { 44 | name string 45 | args args 46 | want bool 47 | }{ 48 | { 49 | name: "missing api", 50 | args: args{ 51 | config: &Config{ 52 | Target: "contents", 53 | ServiceID: "backup-test", 54 | Contents: ContentsConfig{ 55 | GetPublishContentsAPIKey: publishAPIKey, 56 | Endpoints: []string{"missing"}, 57 | RequestUnit: 10, 58 | ClassifyByStatus: false, 59 | SaveAsCSV: false, 60 | }, 61 | }, 62 | baseDir: "../backup/test/", 63 | }, 64 | want: false, 65 | }, 66 | { 67 | name: "normal", 68 | args: args{ 69 | config: &Config{ 70 | Target: "contents", 71 | ServiceID: "backup-test", 72 | Contents: ContentsConfig{ 73 | GetPublishContentsAPIKey: publishAPIKey, 74 | Endpoints: []string{"test", "test2"}, 75 | RequestUnit: 10, 76 | ClassifyByStatus: false, 77 | SaveAsCSV: false, 78 | }, 79 | }, 80 | baseDir: "../backup/test/", 81 | }, 82 | want: true, 83 | }, 84 | { 85 | name: "classify by status true, save as csv false", 86 | args: args{ 87 | config: &Config{ 88 | Target: "contents", 89 | ServiceID: "backup-test", 90 | Contents: ContentsConfig{ 91 | GetPublishContentsAPIKey: publishAPIKey, 92 | GetAllStatusContentsAPIKey: allStatusAPIKey, 93 | GetContentsMetaDataAPIKey: metaDataAPIKey, 94 | Endpoints: []string{"test", "test2"}, 95 | RequestUnit: 10, 96 | ClassifyByStatus: true, 97 | SaveAsCSV: false, 98 | }, 99 | }, 100 | baseDir: "../backup/test/", 101 | }, 102 | want: true, 103 | }, 104 | { 105 | name: "classify by status false, save as csv true", 106 | args: args{ 107 | config: &Config{ 108 | Target: "contents", 109 | ServiceID: "backup-test", 110 | Contents: ContentsConfig{ 111 | GetPublishContentsAPIKey: publishAPIKey, 112 | Endpoints: []string{"test", "test2"}, 113 | RequestUnit: 10, 114 | ClassifyByStatus: false, 115 | SaveAsCSV: true, 116 | }, 117 | }, 118 | baseDir: "../backup/test/", 119 | }, 120 | want: true, 121 | }, 122 | { 123 | name: "classify by status true, save as csv true", 124 | args: args{ 125 | config: &Config{ 126 | Target: "contents", 127 | ServiceID: "backup-test", 128 | Contents: ContentsConfig{ 129 | GetPublishContentsAPIKey: publishAPIKey, 130 | GetAllStatusContentsAPIKey: allStatusAPIKey, 131 | GetContentsMetaDataAPIKey: metaDataAPIKey, 132 | Endpoints: []string{"test", "test2"}, 133 | RequestUnit: 10, 134 | ClassifyByStatus: true, 135 | SaveAsCSV: true, 136 | }, 137 | }, 138 | baseDir: "../backup/test/", 139 | }, 140 | want: true, 141 | }, 142 | } 143 | for _, tt := range tests { 144 | t.Run(tt.name, func(t *testing.T) { 145 | time.Sleep(5 * time.Second) 146 | 147 | client := &Client{} 148 | client.Config = tt.args.config 149 | 150 | err := client.BackupContents(tt.args.baseDir) 151 | got := err == nil 152 | if got != tt.want { 153 | t.Errorf("backupContents() = %v, want %v", got, tt.want) 154 | } 155 | }) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /client/contents.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "bytes" 5 | "encoding/csv" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "log" 10 | "net/http" 11 | "os" 12 | "time" 13 | 14 | "github.com/tidwall/gjson" 15 | ) 16 | 17 | func (c Client) BackupContents(baseDir string) error { 18 | log.Println("コンテンツのバックアップを開始します") 19 | 20 | for _, endpoint := range c.Config.Contents.Endpoints { 21 | log.Printf("%sのバックアップを開始します\n", endpoint) 22 | 23 | // 1:ステータスごとの分類を行う場合 24 | if c.Config.Contents.ClassifyByStatus { 25 | fmt.Println("コンテンツの処理を開始しました") 26 | // 全コンテンツの合計件数を取得 27 | allCotentsCount, err := c.getContentsTotalCount(endpoint, c.Config.Contents.GetAllStatusContentsAPIKey) 28 | if err != nil { 29 | return fmt.Errorf("全コンテンツの合計件数の取得でエラーが発生しました: %w", err) 30 | } 31 | 32 | // 必要なリクエスト回数を計算 33 | requiredRequestCount := (allCotentsCount/c.Config.Contents.RequestUnit + 1) 34 | 35 | // 全コンテンツの取得した後、ステータスごとにデータを振り分けて保存する 36 | err = c.saveContentsWithStatus(endpoint, requiredRequestCount, baseDir) 37 | if err != nil { 38 | return fmt.Errorf("コンテンツの保存でエラーが発生しました: %w", err) 39 | } 40 | } else { 41 | // 2:ステータスごとの分類を行わない場合 42 | totalCount, err := c.getContentsTotalCount(endpoint, c.Config.Contents.GetPublishContentsAPIKey) 43 | if err != nil { 44 | return fmt.Errorf("コンテンツの合計件数の取得でエラーが発生しました: %w", err) 45 | } 46 | requiredRequestCount := (totalCount/c.Config.Contents.RequestUnit + 1) 47 | 48 | err = c.saveContents(endpoint, requiredRequestCount, baseDir, c.Config.Contents.GetPublishContentsAPIKey, "PUBLISH") 49 | if err != nil { 50 | return fmt.Errorf("コンテンツの保存でエラーが発生しました: %w", err) 51 | } 52 | } 53 | } 54 | return nil 55 | } 56 | 57 | func (c Client) getContentsTotalCount(endpoint string, apiKey string) (int, error) { 58 | req, _ := http.NewRequest( 59 | "GET", 60 | fmt.Sprintf("https://%s.microcms.io/api/v1/%s?limit=0", c.Config.ServiceID, endpoint), 61 | nil) 62 | req.Header.Set("X-MICROCMS-API-KEY", apiKey) 63 | 64 | client := new(http.Client) 65 | resp, err := client.Do(req) 66 | 67 | if err != nil { 68 | return 0, err 69 | } 70 | 71 | if resp.StatusCode != http.StatusOK { 72 | return 0, fmt.Errorf("ステータスコード:%d 正常にレスポンスを取得できませんでした", resp.StatusCode) 73 | } 74 | defer resp.Body.Close() 75 | 76 | body, err := io.ReadAll(resp.Body) 77 | if err != nil { 78 | return 0, err 79 | } 80 | 81 | response := &ContentsAPIResponse{} 82 | err = json.Unmarshal(body, response) 83 | if err != nil { 84 | return 0, err 85 | } 86 | 87 | return response.TotalCount, err 88 | } 89 | 90 | func (c Client) saveContents(endpoint string, requiredRequestCount int, baseDir string, apiKey string, status string) error { 91 | // CSVファイルとして保存する場合 92 | if c.Config.Contents.SaveAsCSV { 93 | return c.saveContentsAsCSV(endpoint, requiredRequestCount, baseDir, apiKey, status) 94 | } 95 | 96 | // 従来のJSONファイルとして保存する場合 97 | for i := 0; i < requiredRequestCount; i++ { 98 | // 1秒のディレイを追加 99 | if i > 0 { 100 | time.Sleep(1 * time.Second) 101 | } 102 | 103 | client := new(http.Client) 104 | requestURL := fmt.Sprintf("https://%s.microcms.io/api/v1/%s?limit=%d&offset=%d", c.Config.ServiceID, endpoint, c.Config.Contents.RequestUnit, c.Config.Contents.RequestUnit*i) 105 | req, _ := http.NewRequest("GET", requestURL, nil) 106 | req.Header.Set("X-MICROCMS-API-KEY", apiKey) 107 | resp, err := client.Do(req) 108 | if err != nil { 109 | return err 110 | } 111 | if resp.StatusCode != http.StatusOK { 112 | return fmt.Errorf("ステータスコード:%d 正常にレスポンスを取得できませんでした", resp.StatusCode) 113 | } 114 | defer resp.Body.Close() 115 | 116 | // レスポンスボディを読み込む 117 | body, err := io.ReadAll(resp.Body) 118 | if err != nil { 119 | return err 120 | } 121 | 122 | // gjsonでcontents配列を取得 123 | contents := gjson.GetBytes(body, "contents") 124 | if !contents.IsArray() { 125 | return fmt.Errorf("contentsが配列ではありません") 126 | } 127 | 128 | for j, item := range contents.Array() { 129 | number := i*c.Config.Contents.RequestUnit + j + 1 130 | // item.Rawで元の順序のままJSON文字列が得られる 131 | err := c.writeRawJSONWithStatus(item.Raw, baseDir, endpoint, number, status, "") 132 | if err != nil { 133 | return err 134 | } 135 | } 136 | 137 | // 進捗状況の表示 138 | fmt.Printf("[%d / %d] %s\n", i+1, requiredRequestCount, requestURL) 139 | } 140 | 141 | return nil 142 | } 143 | 144 | // saveContentsAsCSV はコンテンツをCSVファイルとして保存する関数 145 | func (c Client) saveContentsAsCSV(endpoint string, requiredRequestCount int, baseDir string, apiKey string, status string) error { 146 | // 保存先ディレクトリを作成 147 | dir, err := makeSaveDir(baseDir, endpoint, status, "") 148 | if err != nil { 149 | return err 150 | } 151 | 152 | // CSVファイルを作成 153 | csvFile, err := os.Create(fmt.Sprintf("%s/contents.csv", dir)) 154 | if err != nil { 155 | return err 156 | } 157 | defer csvFile.Close() 158 | 159 | // CSVライターを作成 160 | writer := csv.NewWriter(csvFile) 161 | defer writer.Flush() 162 | 163 | // すべてのコンテンツで共通のカラムを収集 164 | allKeys := make(map[string]bool) 165 | var allContents []gjson.Result 166 | var orderedKeys []string 167 | 168 | // まずすべてのコンテンツを取得して、存在するすべてのキーを収集 169 | for i := 0; i < requiredRequestCount; i++ { 170 | client := new(http.Client) 171 | requestURL := fmt.Sprintf("https://%s.microcms.io/api/v1/%s?limit=%d&offset=%d", c.Config.ServiceID, endpoint, c.Config.Contents.RequestUnit, c.Config.Contents.RequestUnit*i) 172 | req, _ := http.NewRequest("GET", requestURL, nil) 173 | req.Header.Set("X-MICROCMS-API-KEY", apiKey) 174 | resp, err := client.Do(req) 175 | if err != nil { 176 | return err 177 | } 178 | if resp.StatusCode != http.StatusOK { 179 | return fmt.Errorf("ステータスコード:%d 正常にレスポンスを取得できませんでした", resp.StatusCode) 180 | } 181 | defer resp.Body.Close() 182 | 183 | // レスポンスボディを読み込む 184 | body, err := io.ReadAll(resp.Body) 185 | if err != nil { 186 | return err 187 | } 188 | 189 | // gjsonでcontents配列を取得 190 | contents := gjson.GetBytes(body, "contents") 191 | if !contents.IsArray() { 192 | return fmt.Errorf("contentsが配列ではありません") 193 | } 194 | 195 | // 各コンテンツのキーを収集 196 | for _, item := range contents.Array() { 197 | allContents = append(allContents, item) 198 | // 最初のコンテンツからキーの順序を取得 199 | if len(orderedKeys) == 0 { 200 | item.ForEach(func(key, value gjson.Result) bool { 201 | keyStr := key.String() 202 | if !allKeys[keyStr] { 203 | orderedKeys = append(orderedKeys, keyStr) 204 | allKeys[keyStr] = true 205 | } 206 | return true 207 | }) 208 | } else { 209 | // 2つ目以降のコンテンツでは、新しいキーのみを追加 210 | item.ForEach(func(key, value gjson.Result) bool { 211 | keyStr := key.String() 212 | if !allKeys[keyStr] { 213 | orderedKeys = append(orderedKeys, keyStr) 214 | allKeys[keyStr] = true 215 | } 216 | return true 217 | }) 218 | } 219 | } 220 | 221 | // 進捗状況の表示 222 | fmt.Printf("[%d / %d] %s\n", i+1, requiredRequestCount, requestURL) 223 | } 224 | 225 | // ヘッダー行を書き込む 226 | if err := writer.Write(orderedKeys); err != nil { 227 | return err 228 | } 229 | 230 | // 各コンテンツのデータを書き込む 231 | for _, item := range allContents { 232 | row := make([]string, len(orderedKeys)) 233 | for i, key := range orderedKeys { 234 | value := item.Get(key) 235 | // 値がオブジェクトや配列の場合はJSON文字列として保存 236 | if value.IsObject() || value.IsArray() { 237 | row[i] = value.Raw 238 | } else { 239 | row[i] = value.String() 240 | } 241 | } 242 | if err := writer.Write(row); err != nil { 243 | return err 244 | } 245 | } 246 | 247 | return nil 248 | } 249 | 250 | func (c Client) saveContentsWithStatus(endpoint string, requiredRequestCount int, baseDir string) error { 251 | // すべてのコンテンツで共通のカラムを収集 252 | allKeys := make(map[string]bool) 253 | var allContents []gjson.Result 254 | var orderedKeys []string 255 | 256 | // まずすべてのコンテンツを取得して、存在するすべてのキーを収集 257 | for i := 0; i < requiredRequestCount; i++ { 258 | // 1秒のディレイを追加 259 | if i > 0 { 260 | time.Sleep(2 * time.Second) 261 | } 262 | 263 | // コンテンツAPIから取得 264 | client := new(http.Client) 265 | requestURL := fmt.Sprintf("https://%s.microcms.io/api/v1/%s?limit=%d&offset=%d", c.Config.ServiceID, endpoint, c.Config.Contents.RequestUnit, c.Config.Contents.RequestUnit*i) 266 | req, _ := http.NewRequest("GET", requestURL, nil) 267 | req.Header.Set("X-MICROCMS-API-KEY", c.Config.Contents.GetAllStatusContentsAPIKey) 268 | resp, err := client.Do(req) 269 | if err != nil { 270 | return err 271 | } 272 | if resp.StatusCode != http.StatusOK { 273 | return fmt.Errorf("ステータスコード:%d 正常にレスポンスを取得できませんでした", resp.StatusCode) 274 | } 275 | defer resp.Body.Close() 276 | 277 | // マネジメントAPIから取得 278 | mRequestURL := fmt.Sprintf("https://%s.microcms-management.io/api/v1/contents/%s?limit=%d&offset=%d", c.Config.ServiceID, endpoint, c.Config.Contents.RequestUnit, c.Config.Contents.RequestUnit*i) 279 | mReq, _ := http.NewRequest("GET", mRequestURL, nil) 280 | mReq.Header.Set("X-MICROCMS-API-KEY", c.Config.Contents.GetContentsMetaDataAPIKey) 281 | mResp, err := client.Do(mReq) 282 | if err != nil { 283 | return err 284 | } 285 | if mResp.StatusCode != http.StatusOK { 286 | return fmt.Errorf("ステータスコード:%d 正常にレスポンスを取得できませんでした", mResp.StatusCode) 287 | } 288 | defer mResp.Body.Close() 289 | 290 | // レスポンスボディを読み込む 291 | body, err := io.ReadAll(resp.Body) 292 | if err != nil { 293 | return err 294 | } 295 | 296 | mbody, err := io.ReadAll(mResp.Body) 297 | if err != nil { 298 | return err 299 | } 300 | 301 | // gjsonでcontents配列を取得 302 | contents := gjson.GetBytes(body, "contents") 303 | mContents := gjson.GetBytes(mbody, "contents") 304 | 305 | if !contents.IsArray() || !mContents.IsArray() { 306 | return fmt.Errorf("contentsが配列ではありません") 307 | } 308 | 309 | // 各コンテンツのキーを収集 310 | for j := 0; j < len(contents.Array()); j++ { 311 | item := contents.Array()[j] 312 | allContents = append(allContents, item) 313 | // 最初のコンテンツからキーの順序を取得 314 | if len(orderedKeys) == 0 { 315 | item.ForEach(func(key, value gjson.Result) bool { 316 | keyStr := key.String() 317 | if !allKeys[keyStr] { 318 | orderedKeys = append(orderedKeys, keyStr) 319 | allKeys[keyStr] = true 320 | } 321 | return true 322 | }) 323 | } else { 324 | // 2つ目以降のコンテンツでは、新しいキーのみを追加 325 | item.ForEach(func(key, value gjson.Result) bool { 326 | keyStr := key.String() 327 | if !allKeys[keyStr] { 328 | orderedKeys = append(orderedKeys, keyStr) 329 | allKeys[keyStr] = true 330 | } 331 | return true 332 | }) 333 | } 334 | } 335 | 336 | // 進捗状況の表示 337 | fmt.Printf("[%d / %d] %s\n", i+1, requiredRequestCount, requestURL) 338 | } 339 | 340 | // ステータスごとにコンテンツを分類 341 | statusContents := make(map[string][]gjson.Result) 342 | for i := 0; i < requiredRequestCount; i++ { 343 | // 1秒のディレイを追加 344 | if i > 0 { 345 | time.Sleep(1 * time.Second) 346 | } 347 | 348 | // コンテンツAPIから取得 349 | client := new(http.Client) 350 | requestURL := fmt.Sprintf("https://%s.microcms.io/api/v1/%s?limit=%d&offset=%d", c.Config.ServiceID, endpoint, c.Config.Contents.RequestUnit, c.Config.Contents.RequestUnit*i) 351 | req, _ := http.NewRequest("GET", requestURL, nil) 352 | req.Header.Set("X-MICROCMS-API-KEY", c.Config.Contents.GetAllStatusContentsAPIKey) 353 | resp, err := client.Do(req) 354 | if err != nil { 355 | return err 356 | } 357 | if resp.StatusCode != http.StatusOK { 358 | return fmt.Errorf("ステータスコード:%d 正常にレスポンスを取得できませんでした", resp.StatusCode) 359 | } 360 | defer resp.Body.Close() 361 | 362 | // マネジメントAPIから取得 363 | mRequestURL := fmt.Sprintf("https://%s.microcms-management.io/api/v1/contents/%s?limit=%d&offset=%d", c.Config.ServiceID, endpoint, c.Config.Contents.RequestUnit, c.Config.Contents.RequestUnit*i) 364 | mReq, _ := http.NewRequest("GET", mRequestURL, nil) 365 | mReq.Header.Set("X-MICROCMS-API-KEY", c.Config.Contents.GetContentsMetaDataAPIKey) 366 | mResp, err := client.Do(mReq) 367 | if err != nil { 368 | return err 369 | } 370 | if mResp.StatusCode != http.StatusOK { 371 | return fmt.Errorf("ステータスコード:%d 正常にレスポンスを取得できませんでした", mResp.StatusCode) 372 | } 373 | defer mResp.Body.Close() 374 | 375 | // レスポンスボディを読み込む 376 | body, err := io.ReadAll(resp.Body) 377 | if err != nil { 378 | return err 379 | } 380 | 381 | mbody, err := io.ReadAll(mResp.Body) 382 | if err != nil { 383 | return err 384 | } 385 | 386 | // gjsonでcontents配列を取得 387 | contents := gjson.GetBytes(body, "contents") 388 | mContents := gjson.GetBytes(mbody, "contents") 389 | 390 | if !contents.IsArray() || !mContents.IsArray() { 391 | return fmt.Errorf("contentsが配列ではありません") 392 | } 393 | 394 | for j := 0; j < len(contents.Array()); j++ { 395 | item := contents.Array()[j] 396 | mItem := mContents.Array()[j] 397 | 398 | id := item.Get("id").String() 399 | mid := mItem.Get("id").String() 400 | if id != mid { 401 | return fmt.Errorf("コンテンツIDが一致しませんでした:%s,%s", id, mid) 402 | } 403 | 404 | status := mItem.Get("status.0").String() 405 | 406 | switch status { 407 | case "PUBLISH", "DRAFT", "CLOSED": 408 | statusContents[status] = append(statusContents[status], item) 409 | case "PUBLISH_AND_DRAFT": 410 | // 下書き保存 411 | statusContents["DRAFT"] = append(statusContents["DRAFT"], item) 412 | // 1秒のディレイを追加 413 | time.Sleep(1 * time.Second) 414 | // 公開中データ取得 415 | publishItem, err := c.getContentWithGJSON(endpoint, c.Config.Contents.GetPublishContentsAPIKey, id) 416 | if err != nil { 417 | log.Fatalf("公開中かつ下書き中コンテンツにおいて、公開中のコンテンツの取得に失敗しました: %v", err) 418 | } 419 | statusContents["PUBLISH"] = append(statusContents["PUBLISH"], publishItem) 420 | default: 421 | fmt.Println("未知のステータスです") 422 | } 423 | } 424 | } 425 | 426 | // 各ステータスごとにCSVファイルを作成 427 | for status, contents := range statusContents { 428 | // 保存先ディレクトリを作成 429 | dir, err := makeSaveDir(baseDir, endpoint, status, "") 430 | if err != nil { 431 | return err 432 | } 433 | 434 | if c.Config.Contents.SaveAsCSV { 435 | // CSVファイルを作成 436 | csvFile, err := os.Create(fmt.Sprintf("%s/contents.csv", dir)) 437 | if err != nil { 438 | return err 439 | } 440 | defer csvFile.Close() 441 | 442 | // CSVライターを作成 443 | writer := csv.NewWriter(csvFile) 444 | defer writer.Flush() 445 | 446 | // ヘッダー行を書き込む 447 | if err := writer.Write(orderedKeys); err != nil { 448 | return err 449 | } 450 | 451 | // 各コンテンツのデータを書き込む 452 | for _, item := range contents { 453 | row := make([]string, len(orderedKeys)) 454 | for i, key := range orderedKeys { 455 | value := item.Get(key) 456 | // 値がオブジェクトや配列の場合はJSON文字列として保存 457 | if value.IsObject() || value.IsArray() { 458 | row[i] = value.Raw 459 | } else { 460 | row[i] = value.String() 461 | } 462 | } 463 | if err := writer.Write(row); err != nil { 464 | return err 465 | } 466 | } 467 | } else { 468 | // JSONファイルとして保存 469 | for i, item := range contents { 470 | err := c.writeRawJSONWithStatus(item.Raw, baseDir, endpoint, i+1, status, "") 471 | if err != nil { 472 | return err 473 | } 474 | } 475 | } 476 | } 477 | 478 | return nil 479 | } 480 | 481 | // itemRawはJSON文字列 482 | func (c Client) writeRawJSONWithStatus(itemRaw string, baseDir, endpoint string, number int, status, draftStatusDetail string) error { 483 | // JSONを整形 484 | formattedJson, err := formatJson(itemRaw) 485 | if err != nil { 486 | return err 487 | } 488 | 489 | dir, err := makeSaveDir(baseDir, endpoint, status, draftStatusDetail) 490 | if err != nil { 491 | return err 492 | } 493 | f, err := os.Create(fmt.Sprintf("%s/%d.json", dir, number)) 494 | if err != nil { 495 | return err 496 | } 497 | defer f.Close() 498 | _, err = f.WriteString(formattedJson) 499 | return err 500 | } 501 | 502 | // 公開中データ取得用 503 | func (c Client) getContentWithGJSON(endpoint, apiKey, contentId string) (gjson.Result, error) { 504 | url := fmt.Sprintf("https://%s.microcms.io/api/v1/%s/%s", c.Config.ServiceID, endpoint, contentId) 505 | req, _ := http.NewRequest("GET", url, nil) 506 | req.Header.Set("X-MICROCMS-API-KEY", apiKey) 507 | 508 | client := new(http.Client) 509 | resp, err := client.Do(req) 510 | if err != nil { 511 | return gjson.Result{}, err 512 | } 513 | defer resp.Body.Close() 514 | if resp.StatusCode != http.StatusOK { 515 | return gjson.Result{}, fmt.Errorf("ステータスコード:%d 正常にレスポンスを取得できませんでした", resp.StatusCode) 516 | } 517 | body, err := io.ReadAll(resp.Body) 518 | if err != nil { 519 | return gjson.Result{}, err 520 | } 521 | return gjson.ParseBytes(body), nil 522 | } 523 | 524 | func formatJson(rawJson string) (string, error) { 525 | var buf bytes.Buffer 526 | err := json.Indent(&buf, []byte(rawJson), "", " ") 527 | return buf.String(), err 528 | } 529 | 530 | func makeSaveDir(baseDir string, endpoint string, status string, draftStatusDetail string) (string, error) { 531 | contentsDir := baseDir + "contents/" + endpoint + "/" + status + "/" + draftStatusDetail 532 | err := os.MkdirAll(contentsDir, os.ModePerm) 533 | return contentsDir, err 534 | } 535 | --------------------------------------------------------------------------------