/plugin/backblaze/plugin.go

https://github.com/starkandwayne/shield · Go · 291 lines · 247 code · 41 blank · 3 comment · 52 complexity · 1e6c77a1613803c2bc8d9053c746789b MD5 · raw file

  1. package main
  2. import (
  3. "context"
  4. "io"
  5. "os"
  6. "regexp"
  7. "strings"
  8. "time"
  9. fmt "github.com/jhunt/go-ansi"
  10. "github.com/kurin/blazer/b2"
  11. "github.com/shieldproject/shield/plugin"
  12. )
  13. const DefaultPrefix = ""
  14. func validBucketName(v string) bool {
  15. ok, err := regexp.MatchString(`^[a-z0-9][a-z0-9\.\-]{1,61}[a-z0-9]$`, v)
  16. return ok && err == nil
  17. }
  18. func main() {
  19. p := BackblazePlugin{
  20. Name: "Backblaze Storage Plugin",
  21. Author: "SHIELD Core Team",
  22. Version: "0.0.1",
  23. Features: plugin.PluginFeatures{
  24. Target: "no",
  25. Store: "yes",
  26. },
  27. Example: `
  28. {
  29. "access_key_id" : "your-access-key-id", # REQUIRED
  30. "secret_access_key" : "your-secret-access-key", # REQUIRED
  31. "bucket" : "name-of-your-bucket", # REQUIRED
  32. "prefix" : "/path/in/bucket", # where to store archives, inside the bucket
  33. }
  34. `,
  35. Defaults: `
  36. {
  37. }
  38. `,
  39. Fields: []plugin.Field{
  40. plugin.Field{
  41. Mode: "store",
  42. Name: "access_key_id",
  43. Type: "string",
  44. Title: "Access Key ID",
  45. Help: "The Access Key ID to use when authenticating against B2.",
  46. },
  47. plugin.Field{
  48. Mode: "store",
  49. Name: "secret_access_key",
  50. Type: "password",
  51. Title: "Secret Access Key",
  52. Help: "The Secret Access Key to use when authenticating against B2.",
  53. },
  54. plugin.Field{
  55. Mode: "store",
  56. Name: "bucket",
  57. Type: "string",
  58. Title: "Bucket Name",
  59. Help: "Name of the bucket to store backup archives in.",
  60. Example: "my-aws-backups",
  61. Required: true,
  62. },
  63. plugin.Field{
  64. Mode: "store",
  65. Name: "prefix",
  66. Type: "string",
  67. Title: "Bucket Path Prefix",
  68. Help: "An optional sub-path of the bucket to use for storing archives. By default, archives are stored in the root of the bucket.",
  69. },
  70. },
  71. }
  72. plugin.Run(p)
  73. }
  74. type BackblazePlugin plugin.PluginInfo
  75. type backblazeEndpoint struct {
  76. AccessKey string
  77. SecretKey string
  78. PathPrefix string
  79. Bucket string
  80. }
  81. func (p BackblazePlugin) Meta() plugin.PluginInfo {
  82. return plugin.PluginInfo(p)
  83. }
  84. func (p BackblazePlugin) Validate(endpoint plugin.ShieldEndpoint) error {
  85. var (
  86. s string
  87. err error
  88. fail bool
  89. )
  90. //BEGIN AUTH VALIDATION
  91. s, err = endpoint.StringValue("access_key_id")
  92. if err != nil {
  93. fmt.Printf("@R{\u2717 access_key_id %s}\n", err)
  94. fail = true
  95. } else {
  96. fmt.Printf("@G{\u2713 access_key_id} @C{%s}\n", plugin.Redact(s))
  97. }
  98. s, err = endpoint.StringValue("secret_access_key")
  99. if err != nil {
  100. fmt.Printf("@R{\u2717 secret_access_key %s}\n", err)
  101. fail = true
  102. } else {
  103. fmt.Printf("@G{\u2713 secret_access_key} @C{%s}\n", plugin.Redact(s))
  104. }
  105. //END AUTH VALIDATION
  106. s, err = endpoint.StringValue("bucket")
  107. if err != nil {
  108. fmt.Printf("@R{\u2717 bucket %s}\n", err)
  109. fail = true
  110. } else if !validBucketName(s) {
  111. fmt.Printf("@R{\u2717 bucket '%s' is an invalid bucket name (must be all lowercase)}\n", s)
  112. fail = true
  113. } else {
  114. fmt.Printf("@G{\u2713 bucket} @C{%s}\n", plugin.Redact(s))
  115. }
  116. s, err = endpoint.StringValueDefault("prefix", DefaultPrefix)
  117. if err != nil {
  118. fmt.Printf("@R{\u2717 prefix %s}\n", err)
  119. fail = true
  120. } else if s == "" {
  121. fmt.Printf("@G{\u2713 prefix} (none)\n")
  122. } else {
  123. s = strings.TrimLeft(s, "/")
  124. fmt.Printf("@G{\u2713 prefix} @C{%s}\n", s)
  125. }
  126. if fail {
  127. return fmt.Errorf("b2: invalid configuration")
  128. }
  129. return nil
  130. }
  131. func (p BackblazePlugin) Backup(endpoint plugin.ShieldEndpoint) error {
  132. return plugin.UNIMPLEMENTED
  133. }
  134. func (p BackblazePlugin) Restore(endpoint plugin.ShieldEndpoint) error {
  135. return plugin.UNIMPLEMENTED
  136. }
  137. func (p BackblazePlugin) Store(endpoint plugin.ShieldEndpoint) (string, int64, error) {
  138. c, err := getB2ConnInfo(endpoint)
  139. if err != nil {
  140. return "", 0, err
  141. }
  142. client, err := c.Connect()
  143. if err != nil {
  144. return "", 0, err
  145. }
  146. path := c.genBackupPath()
  147. plugin.DEBUG("Storing data in %s", path)
  148. ctx := context.Background()
  149. bucket, err := client.Bucket(ctx, c.Bucket)
  150. if err != nil {
  151. return "", 0, err
  152. }
  153. obj := bucket.Object(strings.TrimPrefix(path, "/"))
  154. w := obj.NewWriter(ctx)
  155. io.Copy(w, os.Stdin)
  156. if err != nil {
  157. return "", 0, err
  158. }
  159. w.Close()
  160. if err != nil {
  161. return "", 0, err
  162. }
  163. var size int64
  164. size = 1024
  165. return path, size, nil
  166. }
  167. func (p BackblazePlugin) Retrieve(endpoint plugin.ShieldEndpoint, file string) error {
  168. e, err := getB2ConnInfo(endpoint)
  169. if err != nil {
  170. return err
  171. }
  172. client, err := e.Connect()
  173. if err != nil {
  174. return err
  175. }
  176. ctx := context.Background()
  177. bucket, err := client.Bucket(ctx, e.Bucket)
  178. if err != nil {
  179. return err
  180. }
  181. obj := bucket.Object(strings.TrimPrefix(file, "/"))
  182. reader := obj.NewReader(ctx)
  183. if err != nil {
  184. return err
  185. }
  186. _, err = io.Copy(os.Stdout, reader)
  187. return err
  188. }
  189. func (p BackblazePlugin) Purge(endpoint plugin.ShieldEndpoint, file string) error {
  190. e, err := getB2ConnInfo(endpoint)
  191. if err != nil {
  192. return err
  193. }
  194. client, err := e.Connect()
  195. if err != nil {
  196. return err
  197. }
  198. ctx := context.Background()
  199. bucket, err := client.Bucket(ctx, e.Bucket)
  200. if err != nil {
  201. return err
  202. }
  203. obj := bucket.Object(strings.TrimPrefix(file, "/"))
  204. return obj.Delete(ctx)
  205. }
  206. func getB2ConnInfo(e plugin.ShieldEndpoint) (backblazeEndpoint, error) {
  207. var (
  208. key string
  209. secret string
  210. err error
  211. )
  212. key, err = e.StringValue("access_key_id")
  213. if err != nil {
  214. return backblazeEndpoint{}, err
  215. }
  216. secret, err = e.StringValue("secret_access_key")
  217. if err != nil {
  218. return backblazeEndpoint{}, err
  219. }
  220. bucket, err := e.StringValue("bucket")
  221. if err != nil {
  222. return backblazeEndpoint{}, err
  223. }
  224. prefix, err := e.StringValueDefault("prefix", DefaultPrefix)
  225. if err != nil {
  226. return backblazeEndpoint{}, err
  227. }
  228. prefix = strings.TrimLeft(prefix, "/")
  229. return backblazeEndpoint{
  230. AccessKey: key,
  231. SecretKey: secret,
  232. PathPrefix: prefix,
  233. Bucket: bucket,
  234. }, nil
  235. }
  236. func (e backblazeEndpoint) genBackupPath() string {
  237. t := time.Now()
  238. year, mon, day := t.Date()
  239. hour, min, sec := t.Clock()
  240. uuid := plugin.GenUUID()
  241. path := fmt.Sprintf("%s/%04d/%02d/%02d/%04d-%02d-%02d-%02d%02d%02d-%s", e.PathPrefix, year, mon, day, year, mon, day, hour, min, sec, uuid)
  242. // Remove double slashes
  243. path = strings.Replace(path, "//", "/", -1)
  244. return path
  245. }
  246. func (e backblazeEndpoint) Connect() (*b2.Client, error) {
  247. ctx := context.Background()
  248. return b2.NewClient(ctx, e.AccessKey, e.SecretKey)
  249. }