├── testdata
├── group1
│ ├── empty.js
│ ├── exclude.txt
│ ├── exclude
│ │ └── example.txt
│ ├── example.js
│ ├── favicon.ico
│ ├── sub
│ │ └── heart.gif
│ ├── 论雪莱《爱的哲学》中的根隐喻.pdf
│ └── index.html
└── group2
│ ├── empty.js
│ ├── example.js
│ └── favicon.ico
├── .gitignore
├── favicon.ico
├── Dockerfile
├── utils
├── env.go
├── timeCost.go
├── action.go
├── error.go
├── hash.go
├── match.go
├── ext.go
├── match_test.go
└── walkDir.go
├── .env
├── go.mod
├── .github
└── workflows
│ ├── dockerhub.yml
│ └── test.yml
├── operation
├── website.go
├── incremental.go
├── delete.go
└── upload.go
├── main.go
├── config
└── config.go
├── go.sum
├── action.yml
├── README.md
└── main_test.go
/testdata/group1/empty.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/testdata/group1/exclude.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/testdata/group2/empty.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/testdata/group1/exclude/example.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/testdata/group1/example.js:
--------------------------------------------------------------------------------
1 | console.log('group1')
--------------------------------------------------------------------------------
/testdata/group2/example.js:
--------------------------------------------------------------------------------
1 | console.log('group2')
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode
2 | dist
3 | .env.local
4 | .DS_Store
--------------------------------------------------------------------------------
/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fangbinwei/aliyun-oss-website-action/HEAD/favicon.ico
--------------------------------------------------------------------------------
/testdata/group1/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fangbinwei/aliyun-oss-website-action/HEAD/testdata/group1/favicon.ico
--------------------------------------------------------------------------------
/testdata/group2/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fangbinwei/aliyun-oss-website-action/HEAD/testdata/group2/favicon.ico
--------------------------------------------------------------------------------
/testdata/group1/sub/heart.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fangbinwei/aliyun-oss-website-action/HEAD/testdata/group1/sub/heart.gif
--------------------------------------------------------------------------------
/testdata/group1/论雪莱《爱的哲学》中的根隐喻.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fangbinwei/aliyun-oss-website-action/HEAD/testdata/group1/论雪莱《爱的哲学》中的根隐喻.pdf
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.16
2 |
3 | WORKDIR /go/src/github.com/fangbinwei/aliyun-oss-website-action
4 | COPY . .
5 | RUN go get -d -v ./...
6 | RUN go install -v ./...
7 |
8 | # run in /github/workspace
9 | CMD ["aliyun-oss-website-action"]
--------------------------------------------------------------------------------
/utils/env.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import "os"
4 |
5 | // Getenv with fallback
6 | func Getenv(key, fallback string) string {
7 | if value, ok := os.LookupEnv(key); ok {
8 | return value
9 | }
10 | return fallback
11 | }
12 |
--------------------------------------------------------------------------------
/testdata/group1/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Document
7 |
8 |
9 | test
10 |
11 |
--------------------------------------------------------------------------------
/utils/timeCost.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "fmt"
5 | "time"
6 | )
7 | // TimeCost is time-consuming calculation function
8 | // Usage: defer TimeCost()()
9 | func TimeCost() func() {
10 | start := time.Now()
11 | return func() {
12 | tc := time.Since(start)
13 | fmt.Printf("time cost = %v\n", tc)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | ENDPOINT = fangbinwei-blog.oss-cn-shanghai.aliyuncs.com
2 | FOLDER = testdata/group1
3 | BUCKET = fangbinwei-blog
4 | INDEX_PAGE = index.html
5 | NOT_FOUND_PAGE = 404.html
6 | CNAME = true
7 | HTML_CACHE_CONTROL = no-cache
8 | IMAGE_CACHE_CONTROL = max-age=864000
9 | OTHER_CACHE_CONTROL = max-age=2592000
10 | SKIP_SETTING = false
11 | INCREMENTAL = true
--------------------------------------------------------------------------------
/utils/action.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "strings"
5 | )
6 |
7 | // GetActionInputAsSlice handle the action multiline input as slice
8 | func GetActionInputAsSlice(input string) []string {
9 | result := make([]string, 0, 5)
10 | s := strings.Split(input, "\n")
11 | for _, i := range s {
12 | if c := strings.TrimSpace(i); c != "" {
13 | result = append(result, c)
14 | }
15 | }
16 | return result
17 | }
18 |
--------------------------------------------------------------------------------
/utils/error.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | )
7 |
8 | // HandleError is error handling method, print error and exit
9 | func HandleError(err error) {
10 | fmt.Println("occurred error:", err)
11 | os.Exit(1)
12 | }
13 |
14 | // LogErrors is used to print []error
15 | func LogErrors(errs []error) {
16 | if errs != nil {
17 | fmt.Println("Errors:")
18 | for i, err := range errs {
19 | fmt.Printf("%d\n%v\n", i, err)
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module aliyun-oss-website-action
2 |
3 | go 1.13
4 |
5 | require (
6 | github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f // indirect
7 | github.com/fangbinwei/aliyun-oss-go-sdk v2.1.5-0.20200802190536-e2778bb3ae43+incompatible
8 | github.com/joho/godotenv v1.3.0
9 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
10 | github.com/satori/go.uuid v1.2.0 // indirect
11 | github.com/stretchr/testify v1.6.1
12 | golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect
13 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
14 | )
15 |
--------------------------------------------------------------------------------
/utils/hash.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "crypto/md5"
5 | "encoding/base64"
6 | "io"
7 | "os"
8 | )
9 |
10 | func HashMD5(filepath string) (string, error) {
11 | f, err := os.Open(filepath)
12 | if err != nil {
13 | // TODO: debug info
14 | return "", err
15 | }
16 | defer f.Close()
17 | return hashMD5(f)
18 |
19 | }
20 |
21 | func hashMD5(f io.Reader) (string, error) {
22 | h := md5.New()
23 | if _, err := io.Copy(h, f); err != nil {
24 | // TODO: debug info
25 | return "", err
26 | }
27 | result := h.Sum(nil)
28 | encoded := base64.StdEncoding.EncodeToString(result)
29 |
30 | return encoded, nil
31 | }
32 |
--------------------------------------------------------------------------------
/utils/match.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "path"
5 | "strings"
6 | )
7 |
8 | // Match path with one of patterns
9 | func Match(patterns []string, ossPath string) bool {
10 | for _, p := range patterns {
11 | if match(p, ossPath) {
12 | return true
13 | }
14 | }
15 | return false
16 | }
17 |
18 | func match(pattern string, ossPath string) bool {
19 | pattern = strings.TrimPrefix(pattern, "./")
20 | if hasMeta(pattern) {
21 | match, err := path.Match(pattern, ossPath)
22 | if err != nil {
23 | return false
24 | }
25 | return match
26 | }
27 | if !strings.HasPrefix(ossPath, pattern) {
28 | return false
29 | }
30 |
31 | // dir
32 | if strings.HasSuffix(pattern, "/") {
33 | return true
34 | }
35 | // file
36 | return ossPath == pattern
37 | }
38 |
39 | func hasMeta(p string) bool {
40 | return strings.IndexAny(p, "*?[") >= 0
41 | }
42 |
--------------------------------------------------------------------------------
/utils/ext.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "path"
5 | "strings"
6 | )
7 |
8 | // IsHTML is used to determine if a file is HTML
9 | func IsHTML(filename string) bool {
10 | return strings.HasSuffix(strings.ToLower(filename), ".html")
11 | }
12 |
13 | // IsPDF is used to determine if a file is PDF
14 | func IsPDF(filename string) bool {
15 | return strings.HasSuffix(strings.ToLower(filename), ".pdf")
16 | }
17 |
18 | // IsImage is used to determine if a file is image
19 | func IsImage(filename string) bool {
20 | imageExts := []string{
21 | ".png",
22 | ".jpg",
23 | ".jpeg",
24 | ".webp",
25 | ".gif",
26 | ".bmp",
27 | ".tiff",
28 | ".ico",
29 | ".svg",
30 | }
31 | return func() bool {
32 | ext := path.Ext(filename)
33 | for _, e := range imageExts {
34 | if e == ext {
35 | return true
36 | }
37 | }
38 | return false
39 | }()
40 | }
41 |
--------------------------------------------------------------------------------
/.github/workflows/dockerhub.yml:
--------------------------------------------------------------------------------
1 | name: docker build&upload
2 |
3 | on:
4 | # 支持手动触发
5 | workflow_dispatch:
6 | inputs:
7 | tag:
8 | default: 'latest'
9 | description: dockerhub tag
10 | push:
11 | branches:
12 | - 'master'
13 |
14 | jobs:
15 | docker:
16 | runs-on: ubuntu-latest
17 | steps:
18 | -
19 | name: Checkout
20 | uses: actions/checkout@v2
21 | -
22 | name: Set up QEMU
23 | uses: docker/setup-qemu-action@v1
24 | -
25 | name: Set up Docker Buildx
26 | uses: docker/setup-buildx-action@v1
27 | -
28 | name: Login to DockerHub
29 | uses: docker/login-action@v1
30 | with:
31 | username: ${{ secrets.DOCKERHUB_USERNAME }}
32 | password: ${{ secrets.DOCKERHUB_TOKEN }}
33 | -
34 | name: Build and push
35 | uses: docker/build-push-action@v2
36 | with:
37 | context: .
38 | push: true
39 | tags: ${{ format('fangbinwei/aliyun-oss-website-action:{0}', github.event.inputs.tag || 'latest') }}
40 |
--------------------------------------------------------------------------------
/utils/match_test.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | type mock struct {
10 | pattern string
11 | ossPath string
12 | expect bool
13 | }
14 |
15 | func TestMatch(t *testing.T) {
16 | assert := assert.New(t)
17 |
18 | matches := []mock{
19 | {"dir/", "dir", false},
20 | {"dir/", "dir/sub", true},
21 | {"./dir/", "dir/file", true},
22 | {"dir/", "dir/dir2/file", true},
23 | {"dir2/", "dir/dir2/file", false},
24 |
25 | {"file", "file", true},
26 | {"file", "file1", false},
27 | {"file*", "file1", true},
28 | {"file", "file/", false},
29 | {"file", "dir/file", false},
30 | {"dir/*.js", "dir/file.js", true},
31 | {"dir*/", "dir1/", true},
32 | {"dir/*.js", "dir/file.css", false},
33 | {"dir/*.js", "dir/dir2/file.js", false},
34 | {"dir/*/*.js", "dir/dir2/file.js", true},
35 | {"dir/**/*.js", "dir/dir2/dir3/file.js", false},
36 | }
37 |
38 | for _, item := range matches {
39 | if item.expect {
40 | assert.True(match(item.pattern, item.ossPath), item)
41 | } else {
42 | assert.False(match(item.pattern, item.ossPath), item)
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/operation/website.go:
--------------------------------------------------------------------------------
1 | package operation
2 |
3 | import (
4 | "fmt"
5 |
6 | "aliyun-oss-website-action/config"
7 |
8 | "github.com/fangbinwei/aliyun-oss-go-sdk/oss"
9 | )
10 |
11 | // SetStaticWebsiteConfig is used to set some option of website, like redirect strategy, index page, 404 page.
12 | func SetStaticWebsiteConfig() error {
13 | bEnable := true
14 | supportSubDirType := 0
15 | websiteDetailConfig, err := config.Client.GetBucketWebsite(config.Bucket.BucketName)
16 | if err != nil {
17 | serviceError, ok := err.(oss.ServiceError)
18 | // 404 means NoSuchWebsiteConfiguration
19 | if !ok || serviceError.StatusCode != 404 {
20 | fmt.Println("Failed to get website detail configuration, skip setting", err)
21 | return err
22 | }
23 | }
24 | wxml := oss.WebsiteXML(websiteDetailConfig)
25 | wxml.IndexDocument.Suffix = config.IndexPage
26 | wxml.ErrorDocument.Key = config.NotFoundPage
27 | wxml.IndexDocument.SupportSubDir = &bEnable
28 | wxml.IndexDocument.Type = &supportSubDirType
29 |
30 | err = config.Client.SetBucketWebsiteDetail(config.BucketName, wxml)
31 | if err != nil {
32 | fmt.Printf("Failed to set website detail configuration: %v\n", err)
33 | return err
34 | }
35 | return nil
36 | }
37 |
--------------------------------------------------------------------------------
/utils/walkDir.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "fmt"
5 | "io/ioutil"
6 | "os"
7 | "path/filepath"
8 | "sync"
9 | )
10 |
11 | var sema = make(chan struct{}, 20)
12 |
13 | // FileInfoType is a type which contains dir and os.FileInfo
14 | type FileInfoType struct {
15 | Dir string
16 | Path string
17 | PathOSS string
18 | Name string
19 | CacheControl string // Complete 'CacheControl' when uploading files
20 | ContentMD5 string
21 | ValidHash bool // if ContentMD5 is valid
22 | }
23 |
24 | // WalkDir get sub files of target dir
25 | func WalkDir(root string) <-chan FileInfoType {
26 | fileInfos := make(chan FileInfoType, 100)
27 | var sw sync.WaitGroup
28 | sw.Add(1)
29 | go func() {
30 | walkDir(root, &sw, fileInfos)
31 | }()
32 | go func() {
33 | sw.Wait()
34 | close(fileInfos)
35 | }()
36 | return fileInfos
37 | }
38 |
39 | func walkDir(dir string, sw *sync.WaitGroup, fileInfos chan<- FileInfoType) {
40 | defer sw.Done()
41 | for _, entry := range dirents(dir) {
42 | entryName := entry.Name()
43 | if entry.IsDir() {
44 | sw.Add(1)
45 | subdir := filepath.Join(dir, entryName)
46 | go walkDir(subdir, sw, fileInfos)
47 | } else {
48 | p := filepath.Join(dir, entryName)
49 | contentMD5, _ := HashMD5(p)
50 | fileInfos <- FileInfoType{
51 | ValidHash: contentMD5 != "",
52 | ContentMD5: contentMD5,
53 | Dir: dir,
54 | Path: p,
55 | PathOSS: filepath.ToSlash(p),
56 | Name: entryName,
57 | }
58 | }
59 | }
60 | }
61 |
62 | func dirents(dir string) []os.FileInfo {
63 | sema <- struct{}{} // acquire token
64 | defer func() { <-sema }() // release token
65 |
66 | // TOOD: use os.ReadDir
67 | entries, err := ioutil.ReadDir(dir)
68 | if err != nil {
69 | fmt.Printf("dirents error: %v\n", err)
70 | return nil
71 | }
72 | return entries
73 | }
74 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 |
7 | "aliyun-oss-website-action/config"
8 | "aliyun-oss-website-action/operation"
9 | "aliyun-oss-website-action/utils"
10 | )
11 |
12 | func main() {
13 | defer utils.TimeCost()()
14 | if config.Folder == "/" {
15 | fmt.Println("You should not upload the root directory, use ./ instead. 通常来说, 你不应该上传根目录, 也许你是要配置 ./")
16 | os.Exit(1)
17 | }
18 |
19 | if !config.SkipSetting {
20 | operation.SetStaticWebsiteConfig()
21 | } else {
22 | fmt.Println("skip setting static pages related configuration")
23 | }
24 |
25 | var incremental *operation.IncrementalConfig
26 | if config.IsIncremental {
27 | fmt.Println("---- [incremental] ---->")
28 | incremental, _ = operation.GetRemoteIncrementalConfig(config.Bucket)
29 | fmt.Println("<---- [incremental end] ----")
30 | fmt.Println()
31 | }
32 | if !config.IsIncremental || incremental == nil {
33 | // TODO: delete after upload
34 | fmt.Println("---- [delete] ---->")
35 | deleteErrs := operation.DeleteObjects(config.Bucket)
36 | utils.LogErrors(deleteErrs)
37 | fmt.Println("<---- [delete end] ----")
38 | fmt.Println()
39 | }
40 |
41 | records := utils.WalkDir(config.Folder)
42 |
43 | fmt.Println("---- [upload] ---->")
44 | uploaded, uploadErrs := operation.UploadObjects(config.Folder, config.Bucket, records, incremental)
45 | utils.LogErrors(uploadErrs)
46 | fmt.Println("<---- [upload end] ----")
47 | fmt.Println()
48 |
49 | if config.IsIncremental && incremental != nil {
50 | fmt.Println("---- [delete] ---->")
51 | deleteErrs := operation.DeleteObjectsIncremental(config.Bucket, incremental)
52 | utils.LogErrors(deleteErrs)
53 | fmt.Println("<---- [delete end] ----")
54 | fmt.Println()
55 | }
56 |
57 | if config.IsIncremental {
58 | fmt.Println("---- [incremental] ---->")
59 | operation.UploadIncrementalConfig(config.Bucket, uploaded)
60 | fmt.Println("<---- [incremental end] ----")
61 | fmt.Println()
62 | }
63 |
64 | if len(uploadErrs) > 0 {
65 | os.Exit(1)
66 | }
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: deploy test
2 |
3 | on:
4 | # 支持手动触发
5 | workflow_dispatch:
6 | pull_request:
7 | push:
8 | branches:
9 | - 'master'
10 |
11 | jobs:
12 | build:
13 |
14 | runs-on: ubuntu-latest
15 | strategy:
16 | matrix:
17 | node-version: [14.x]
18 |
19 | steps:
20 | # load repo to /github/workspace
21 | - uses: actions/checkout@v2
22 | with:
23 | repository: fangbinwei/blog
24 | fetch-depth: 0
25 | - name: Use Node.js ${{ matrix.node-version }}
26 | uses: actions/setup-node@v1
27 | with:
28 | node-version: ${{ matrix.node-version }}
29 | - run: npm install yarn@1.22.4 -g
30 |
31 | - name: Get yarn cache directory path
32 | id: yarn-cache-dir-path
33 | run: echo "::set-output name=dir::$(yarn cache dir)"
34 |
35 | - uses: actions/cache@v2
36 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
37 | with:
38 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
39 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
40 | restore-keys: |
41 | ${{ runner.os }}-yarn-
42 | - run: yarn install
43 | - run: yarn docs:build
44 | - name: upload files to OSS
45 | uses: fangbinwei/aliyun-oss-website-action@master
46 | with:
47 | accessKeyId: ${{ secrets.ACCESS_KEY_ID }}
48 | accessKeySecret: ${{ secrets.ACCESS_KEY_SECRET }}
49 | bucket: fangbinwei-blog
50 | endpoint: b.fangbinwei.cn
51 | cname: true
52 | # folder in /github/workspace
53 | folder: .vuepress/dist
54 | htmlCacheControl: no-cache
55 | imageCacheControl: max-age=864001
56 | otherCacheControl: max-age=2592001
57 | pdfCacheControl: no-cache
58 | skipSetting: false
59 | # not support recursive pattern **
60 | exclude: |
61 | CNAME
62 | demo1/
63 | demo2/*.md
64 | demo2/*/*.md
--------------------------------------------------------------------------------
/operation/incremental.go:
--------------------------------------------------------------------------------
1 | package operation
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "io"
8 | "sync"
9 |
10 | "github.com/fangbinwei/aliyun-oss-go-sdk/oss"
11 | )
12 |
13 | const INCREMENTAL_CONFIG = ".actioninfo"
14 |
15 | type IncrementalConfig struct {
16 | sync.RWMutex
17 | M map[string]struct {
18 | ContentMD5 string
19 | CacheControl string
20 | }
21 | }
22 |
23 | func (i *IncrementalConfig) stringify() ([]byte, error) {
24 | j, err := json.Marshal(i.M)
25 | return j, err
26 | }
27 |
28 | func (i *IncrementalConfig) parse(raw []byte) error {
29 | err := json.Unmarshal(raw, &(i.M))
30 | return err
31 | }
32 |
33 | func generateIncrementalConfig(uploaded []UploadedObject) ([]byte, error) {
34 | i := new(IncrementalConfig)
35 | i.M = make(map[string]struct {
36 | ContentMD5 string
37 | CacheControl string
38 | })
39 | for _, u := range uploaded {
40 | if !u.ValidHash {
41 | continue
42 | }
43 | i.M[u.ObjectKey] = struct {
44 | ContentMD5 string
45 | CacheControl string
46 | }{
47 | ContentMD5: u.ContentMD5,
48 | CacheControl: u.CacheControl,
49 | }
50 | }
51 | j, err := i.stringify()
52 | return j, err
53 |
54 | }
55 |
56 | func UploadIncrementalConfig(bucket *oss.Bucket, records []UploadedObject) error {
57 | j, err := generateIncrementalConfig(records)
58 | if err != nil {
59 | fmt.Printf("Failed to generate incremental info: %v\n", err)
60 | return err
61 | }
62 |
63 | options := []oss.Option{
64 | oss.ObjectACL(oss.ACLPrivate),
65 | }
66 | err = bucket.PutObject(INCREMENTAL_CONFIG, bytes.NewReader(j), options...)
67 | if err != nil {
68 | fmt.Printf("Failed to upload incremental info: %v\n", err)
69 | return err
70 | }
71 |
72 | fmt.Printf("Update & Upload incremental info: %s\n", INCREMENTAL_CONFIG)
73 | return nil
74 | }
75 |
76 | func GetRemoteIncrementalConfig(bucket *oss.Bucket) (*IncrementalConfig, error) {
77 | c := new(bytes.Buffer)
78 | body, err := bucket.GetObject(INCREMENTAL_CONFIG)
79 | if err != nil {
80 | fmt.Printf("Failed to get remote incremental info: %v\n", err)
81 | return nil, err
82 | }
83 | io.Copy(c, body)
84 | body.Close()
85 | i := new(IncrementalConfig)
86 | err = i.parse(c.Bytes())
87 | if err != nil {
88 | fmt.Printf("Failed to parse remote incremental info: %v\n", err)
89 | return nil, err
90 | }
91 | fmt.Printf("Get remote incremental info: %s\n", INCREMENTAL_CONFIG)
92 |
93 | return i, nil
94 | }
95 |
--------------------------------------------------------------------------------
/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "fmt"
5 | "os"
6 |
7 | "aliyun-oss-website-action/utils"
8 |
9 | "github.com/fangbinwei/aliyun-oss-go-sdk/oss"
10 | "github.com/joho/godotenv"
11 | )
12 |
13 | var (
14 | Endpoint string
15 | AccessKeyID string
16 | AccessKeySecret string
17 | Folder string
18 | Exclude []string
19 | BucketName string
20 | IsCname bool
21 | Client *oss.Client
22 | Bucket *oss.Bucket
23 | SkipSetting bool
24 | IsIncremental bool
25 |
26 | IndexPage string
27 | NotFoundPage string
28 | HTMLCacheControl string
29 | ImageCacheControl string
30 | OtherCacheControl string
31 | PDFCacheControl string
32 | )
33 |
34 | func init() {
35 | godotenv.Load(".env")
36 | godotenv.Load(".env.local")
37 |
38 | Endpoint = os.Getenv("ENDPOINT")
39 | IsCname = os.Getenv("CNAME") == "true"
40 | AccessKeyID = os.Getenv("ACCESS_KEY_ID")
41 | AccessKeySecret = os.Getenv("ACCESS_KEY_SECRET")
42 | Folder = os.Getenv("FOLDER")
43 | Exclude = utils.GetActionInputAsSlice(os.Getenv("EXCLUDE"))
44 | BucketName = os.Getenv("BUCKET")
45 | SkipSetting = os.Getenv("SKIP_SETTING") == "true"
46 | IsIncremental = os.Getenv("INCREMENTAL") == "true"
47 |
48 | IndexPage = utils.Getenv("INDEX_PAGE", "index.html")
49 | NotFoundPage = utils.Getenv("NOT_FOUND_PAGE", "404.html")
50 | HTMLCacheControl = utils.Getenv("HTML_CACHE_CONTROL", "no-cache")
51 | ImageCacheControl = utils.Getenv("IMAGE_CACHE_CONTROL", "max-age=864000")
52 | OtherCacheControl = utils.Getenv("OTHER_CACHE_CONTROL", "max-age=2592000")
53 | PDFCacheControl = utils.Getenv("PDF_CACHE_CONTROL", "max-age=2592000")
54 |
55 | currentPath, err := os.Getwd()
56 | if err != nil {
57 | fmt.Println(err)
58 | }
59 | fmt.Printf("current directory: %s\n", currentPath)
60 | fmt.Printf("endpoint: %s\nbucketName: %s\nfolder: %s\nincremental: %t\nexclude: %v\nindexPage: %s\nnotFoundPage: %s\nisCname: %t\nskipSetting: %t\n",
61 | Endpoint, BucketName, Folder, IsIncremental, Exclude, IndexPage, NotFoundPage, IsCname, SkipSetting)
62 | fmt.Printf("HTMLCacheControl: %s\nimageCacheControl: %s\notherCacheControl: %s\npdfCacheControl: %s\n",
63 | HTMLCacheControl, ImageCacheControl, OtherCacheControl, PDFCacheControl)
64 |
65 | Client, err = oss.New(Endpoint, AccessKeyID, AccessKeySecret, oss.UseCname(IsCname))
66 | if err != nil {
67 | utils.HandleError(err)
68 | }
69 |
70 | Bucket, err = Client.Bucket(BucketName)
71 | if err != nil {
72 | utils.HandleError(err)
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f h1:ZNv7On9kyUzm7fvRZumSyy/IUiSC7AzL0I1jKKtwooA=
2 | github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc=
3 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5 | github.com/fangbinwei/aliyun-oss-go-sdk v2.1.5-0.20200802190536-e2778bb3ae43+incompatible h1:kwCQtEg2LSb0SYlB8OKqg6faDL7MS6lSvEM0cg705XY=
6 | github.com/fangbinwei/aliyun-oss-go-sdk v2.1.5-0.20200802190536-e2778bb3ae43+incompatible/go.mod h1:ugoCgcIlrHXbCHuMbJ8Iq5RGBxrLuP2apphXWDlesY0=
7 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
8 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
9 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
10 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
11 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
12 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
13 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
14 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
15 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
16 | github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
17 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
18 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
19 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
20 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
21 | golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s=
22 | golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
23 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
24 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
25 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
26 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
27 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
28 |
--------------------------------------------------------------------------------
/action.yml:
--------------------------------------------------------------------------------
1 | name: 'aliyun-oss-website-action'
2 | description: 'Deploy your static website on aliyun OSS'
3 | author: 'Binwei Fang '
4 | branding:
5 | icon: 'upload'
6 | color: 'gray-dark'
7 | inputs:
8 | folder:
9 | description: 'Folder which contains the website files'
10 | required: true
11 | exclude:
12 | description: 'Exclude file from the folder'
13 | required: false
14 | accessKeyId:
15 | description: 'Aliyun OSS accessKeyId.'
16 | required: true
17 | accessKeySecret:
18 | description: 'Aliyun OSS accessKeySecret.'
19 | required: true
20 | bucket:
21 | description: 'Aliyun OSS bucket instance.'
22 | required: true
23 | endpoint:
24 | description: 'OSS region domain'
25 | required: true
26 | cname:
27 | description: '`true` to identify the endpoint is your custom domain.'
28 | required: false
29 | default: 'false'
30 | skipSetting:
31 | description: '`true` to skip setting static pages related configuration. `indexPage`, `notFoundPage` will not be used.'
32 | required: false
33 | default: 'false'
34 | incremental:
35 | description: 'Save info of uploaded files to increase next upload speed'
36 | required: false
37 | default: 'true'
38 | indexPage:
39 | description: 'index page'
40 | required: false
41 | default: 'index.html'
42 | notFoundPage:
43 | description: 'not found page'
44 | required: false
45 | default: '404.html'
46 | htmlCacheControl:
47 | description: 'Cache-Control for HTML'
48 | required: false
49 | default: 'no-cache'
50 | imageCacheControl:
51 | description: 'Cache-Control for image'
52 | required: false
53 | default: 'max-age=864000'
54 | pdfCacheControl:
55 | description: 'Cache-Control for PDF'
56 | required: false
57 | default: 'max-age=2592000'
58 | otherCacheControl:
59 | description: 'Cache-Control for other files'
60 | required: false
61 | default: 'max-age=2592000'
62 | runs:
63 | using: 'docker'
64 | image: 'Dockerfile'
65 | env:
66 | ACCESS_KEY_ID: ${{ inputs.accessKeyId }}
67 | ACCESS_KEY_SECRET: ${{ inputs.accessKeySecret }}
68 | BUCKET: ${{ inputs.bucket }}
69 | ENDPOINT: ${{ inputs.endpoint }}
70 | CNAME: ${{ inputs.cname }}
71 | FOLDER: ${{ inputs.folder }}
72 | EXCLUDE: ${{ inputs.exclude }}
73 | SKIP_SETTING: ${{ inputs.skipSetting }}
74 | INCREMENTAL: ${{ inputs.incremental }}
75 | INDEX_PAGE: ${{ inputs.indexPage }}
76 | NOT_FOUND_PAGE: ${{ inputs.notFoundPage }}
77 | HTML_CACHE_CONTROL: ${{ inputs.htmlCacheControl }}
78 | IMAGE_CACHE_CONTROL: ${{ inputs.imageCacheControl }}
79 | OTHER_CACHE_CONTROL: ${{ inputs.otherCacheControl }}
80 | PDF_CACHE_CONTROL: ${{ inputs.pdfCacheControl }}
81 |
82 |
83 |
--------------------------------------------------------------------------------
/operation/delete.go:
--------------------------------------------------------------------------------
1 | package operation
2 |
3 | import (
4 | "fmt"
5 | "sync"
6 |
7 | "aliyun-oss-website-action/utils"
8 |
9 | "github.com/fangbinwei/aliyun-oss-go-sdk/oss"
10 | )
11 |
12 | const maxKeys = 100
13 |
14 | // DeleteObjects is used to delete all objects of the bucket
15 | func DeleteObjects(bucket *oss.Bucket) []error {
16 | var errs []error
17 | objKeyCollection := make(chan string, maxKeys)
18 | go listObjects(bucket, objKeyCollection)
19 |
20 | var sw sync.WaitGroup
21 | var mutex sync.Mutex
22 | tokens := make(chan struct{}, 10)
23 | for k := range objKeyCollection {
24 | sw.Add(1)
25 | go func(key string) {
26 | defer sw.Done()
27 | defer func() {
28 | <-tokens
29 | }()
30 | tokens <- struct{}{}
31 | err := deleteObject(bucket, key)
32 | if err != nil {
33 | mutex.Lock()
34 | errs = append(errs, fmt.Errorf("[FAILED] objectKey: %s\nDetail: %v", key, err))
35 | mutex.Unlock()
36 | return
37 | }
38 | fmt.Printf("objectKey: %s\n", key)
39 | }(k)
40 | }
41 | sw.Wait()
42 |
43 | if len(errs) > 0 {
44 | return errs
45 | }
46 | return nil
47 | }
48 |
49 | func DeleteObjectsIncremental(bucket *oss.Bucket, i *IncrementalConfig) []error {
50 | if i == nil {
51 | return nil
52 | }
53 | // delete incremental info
54 | i.M[INCREMENTAL_CONFIG] = struct {
55 | ContentMD5 string
56 | CacheControl string
57 | }{}
58 |
59 | // TODO: optimize
60 | var errs []error
61 |
62 | var sw sync.WaitGroup
63 | var mutex sync.Mutex
64 | tokens := make(chan struct{}, 10)
65 | for k := range i.M {
66 | sw.Add(1)
67 | go func(key string) {
68 | defer sw.Done()
69 | defer func() {
70 | <-tokens
71 | }()
72 | tokens <- struct{}{}
73 | err := deleteObject(bucket, key)
74 | if err != nil {
75 | mutex.Lock()
76 | errs = append(errs, fmt.Errorf("[FAILED] objectKey: %s\nDetail: %v", key, err))
77 | mutex.Unlock()
78 | return
79 | }
80 | fmt.Printf("objectKey: %s\n", key)
81 | }(k)
82 | }
83 | sw.Wait()
84 |
85 | if len(errs) > 0 {
86 | return errs
87 | }
88 | return nil
89 | }
90 |
91 | func deleteObject(bucket *oss.Bucket, key string) error {
92 | err := bucket.DeleteObject(key)
93 | if err != nil {
94 | return err
95 | }
96 | return nil
97 | }
98 |
99 | func listObjects(bucket *oss.Bucket, objKeyCollection chan<- string) {
100 | marker := oss.Marker("")
101 | for {
102 | lor, err := bucket.ListObjects(oss.MaxKeys(maxKeys), marker)
103 | if err != nil {
104 | utils.HandleError(err)
105 | }
106 | for _, object := range lor.Objects {
107 | objKeyCollection <- object.Key
108 | }
109 | marker = oss.Marker(lor.NextMarker)
110 | if !lor.IsTruncated {
111 | close(objKeyCollection)
112 | break
113 | }
114 | }
115 |
116 | }
117 |
--------------------------------------------------------------------------------
/operation/upload.go:
--------------------------------------------------------------------------------
1 | package operation
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "path"
7 | "strings"
8 | "sync"
9 |
10 | "aliyun-oss-website-action/config"
11 | "aliyun-oss-website-action/utils"
12 |
13 | "github.com/fangbinwei/aliyun-oss-go-sdk/oss"
14 | )
15 |
16 | type UploadedObject struct {
17 | ObjectKey string
18 | Incremental bool
19 | utils.FileInfoType
20 | }
21 |
22 | // UploadObjects upload files to OSS
23 | func UploadObjects(root string, bucket *oss.Bucket, records <-chan utils.FileInfoType, i *IncrementalConfig) ([]UploadedObject, []error) {
24 | if root == "/" {
25 | fmt.Println("You should not upload the root directory, use ./ instead. 通常来说, 你不应该上传根目录, 也许你是要配置 ./")
26 | os.Exit(1)
27 | } else {
28 | root = path.Clean(root) + "/"
29 | }
30 |
31 | var sw sync.WaitGroup
32 | var errorMutex sync.Mutex
33 | var uploadedMutex sync.Mutex
34 | var errs []error
35 | uploaded := make([]UploadedObject, 0, 20)
36 | var tokens = make(chan struct{}, 30)
37 | for item := range records {
38 | sw.Add(1)
39 | go func(item utils.FileInfoType) {
40 | defer sw.Done()
41 | fPath := item.Path
42 | objectKey := strings.TrimPrefix(item.PathOSS, root)
43 | options := getHTTPHeader(&item)
44 |
45 | if shouldExclude(objectKey) {
46 | fmt.Printf("[EXCLUDE] objectKey: %s\n\n", objectKey)
47 | return
48 | }
49 | if shouldSkip(item, objectKey, i) {
50 | fmt.Printf("[SKIP] objectKey: %s \n\n", objectKey)
51 | uploadedMutex.Lock()
52 | uploaded = append(uploaded, UploadedObject{ObjectKey: objectKey, Incremental: true, FileInfoType: item})
53 | uploadedMutex.Unlock()
54 | return
55 | }
56 |
57 | tokens <- struct{}{}
58 | err := bucket.PutObjectFromFile(objectKey, fPath, options...)
59 | <-tokens
60 | if err != nil {
61 | errorMutex.Lock()
62 | errs = append(errs, fmt.Errorf("[FAILED] objectKey: %s\nfilePath: %s\nDetail: %v", objectKey, fPath, err))
63 | errorMutex.Unlock()
64 | return
65 | }
66 | fmt.Printf("objectKey: %s\nfilePath: %s\n\n", objectKey, fPath)
67 | uploadedMutex.Lock()
68 | uploaded = append(uploaded, UploadedObject{ObjectKey: objectKey, FileInfoType: item})
69 | uploadedMutex.Unlock()
70 | }(item)
71 | }
72 | sw.Wait()
73 | if len(errs) > 0 {
74 | return uploaded, errs
75 | }
76 | return uploaded, nil
77 | }
78 |
79 | func getHTTPHeader(item *utils.FileInfoType) []oss.Option {
80 | return []oss.Option{
81 | getCacheControlOption(item),
82 | }
83 | }
84 |
85 | func getCacheControlOption(item *utils.FileInfoType) oss.Option {
86 | var value string
87 | filename := item.Name
88 |
89 | if utils.IsHTML(filename) {
90 | value = config.HTMLCacheControl
91 | } else if utils.IsImage(filename) {
92 | // pic name may not contains hash, so use different strategy
93 | // 10 days
94 | value = config.ImageCacheControl
95 | } else if utils.IsPDF(filename) {
96 | value = config.PDFCacheControl
97 | } else {
98 | // static assets like .js .css, use contentHash in file name, so html can update these files.
99 | // 30 days
100 | value = config.OtherCacheControl
101 | }
102 | item.CacheControl = value
103 | return oss.CacheControl(value)
104 | }
105 |
106 | func shouldExclude(objectKey string) bool {
107 | if utils.Match(config.Exclude, objectKey) {
108 | return true
109 | }
110 | return false
111 | }
112 |
113 | func shouldSkip(item utils.FileInfoType, objectKey string, i *IncrementalConfig) bool {
114 | if i == nil {
115 | return false
116 | }
117 | i.RLock()
118 | remoteConfig, ok := i.M[objectKey]
119 | i.RUnlock()
120 | if !ok {
121 | return false
122 | }
123 | // delete existed objectKey in incremental map, the left is what we should delete
124 | i.Lock()
125 | delete(i.M, objectKey)
126 | i.Unlock()
127 | if item.ValidHash && item.ContentMD5 == remoteConfig.ContentMD5 && item.CacheControl == remoteConfig.CacheControl {
128 | return true
129 | }
130 | return false
131 | }
132 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # aliyun-oss-website-action
2 |
3 | deploy website on aliyun OSS(Alibaba Cloud OSS)
4 |
5 | 将静态网站部署在阿里云OSS
6 |
7 | ## 概览
8 | - 在阿里云OSS创建一个存放网站的bucket
9 | - 准备一个域名, 可能需要备案(bucket选择非大陆区域, 可以不备案, 但是如果CDN加速区域包括大陆, 仍然需要备案)
10 | - 在你的网站repo中, 配置github action, action 触发则**增量上传**网站repo生成的资源文件到bucket中
11 | - 通过阿里云OSS的CDN, 可以很方便地加速网站的访问, 支持HTTPS
12 | > 阿里云HTTPS免费证书停止自动续签, 但是可以自己[申请免费的证书](https://help.aliyun.com/document_detail/156645.htm), 具体解决方案参考[该公告](https://help.aliyun.com/document_detail/479351.html)
13 |
14 | ## Usage
15 |
16 | ```yml
17 | - name: upload files to OSS
18 | uses: fangbinwei/aliyun-oss-website-action@v1
19 | with:
20 | accessKeyId: ${{ secrets.ACCESS_KEY_ID }}
21 | accessKeySecret: ${{ secrets.ACCESS_KEY_SECRET }}
22 | bucket: your-bucket-name
23 | # use your own endpoint
24 | endpoint: oss-cn-shanghai.aliyuncs.com
25 | folder: your-website-output-folder
26 | ```
27 | > 如果你使用了environment secret请[查看这里](#配置了environment-secret怎么不生效)
28 | ### 配置项
29 | - `accessKeyId`: **必填**
30 | - `accessKeySecret`: **必填**
31 | - `endpoint`: **必填**, 支持指定protocol, 例如`https://example.org`或者`http://example.org`
32 | - `folder`: **必填**, repo打包输出的资源文件夹
33 | - `bucket`: **必填**,部署网站的bucket, 用于存放网站的资源
34 | - `indexPage`: 默认`index.html`.网站首页(用于[静态页面配置](#静态页面配置))
35 | - `notFoundPage`: 默认`404.html`.网站404页面(用于[静态页面配置](#静态页面配置))
36 | - `incremental`: 默认`true`. 使用增量上传.
37 | - `skipSetting`: 默认`false`, 是否跳过设置[静态页面配置](#静态页面配置)
38 | - `htmlCacheControl`: 默认`no-cache`
39 | - `imageCacheControl`: 默认`max-age=864000`
40 | - `pdfCacheControl`: 默认`max-age=2592000`
41 | - `otherCacheControl`: 默认`max-age=2592000`
42 | - `exclude`: 不上传`folder`下的某些文件/文件夹
43 | - `cname`: 默认`false`. 若`endpoint`填写自定义域名/bucket域名, 需设置为`true`. (使用CDN的场景下, 不推荐使用自定义域名)
44 |
45 | ## incremental
46 | **开启`incremental`**
47 | 上传文件到OSS后, 还会将文件的`ContentMD5`和`Cache-Control`收集到名为`.actioninfo`的私有文件中. 当再次触发action的时候, 会将待上传的文件信息与`.actioninfo`中记录的信息比对, 信息未发生变化的文件将跳过上传步骤, 只进行增量上传. 且在上传之后, 根据`.actioninfo`和已上传的文件信息, 将OSS中多余的文件进行删除.
48 |
49 | > `.actioninfo` 记录了上一次action执行时, 所上传的文件信息. 私有, 不可公共读写.
50 |
51 | **关闭`incremental`** 或 OSS中不存在`.actioninfo`文件
52 |
53 | 会执行如下步骤
54 | 1. 清除所有OSS中已有的文件
55 | 2. 上传新的文件到OSS中
56 |
57 | > **计划未来优化这个步骤, 优化后, 先上传新的文件到OSS中, 再diff删除多余的文件.**
58 |
59 | ## Cache-Control
60 | 为上传的资源默认设置的`Cache-Control`如下
61 | |资源类型 | Cache-Control|
62 | |----| ----|
63 | |.html|no-cache|
64 | |.png/jpg...(图片资源)|max-age=864000(10days)|
65 | |other|max-age=2592000(30days)|
66 |
67 | ## 静态页面配置
68 | 默认的, action会将阿里云OSS的静态页面配置成如下
69 | 
70 |
71 | 若不需要action来设置, 可以配置`skipSetting`为`true`
72 |
73 | ## exclude
74 | 如果`folder`下的某些文件不需要上传
75 |
76 |
77 | ```yml
78 | - name: exclude some files
79 | uses: fangbinwei/aliyun-oss-website-action@v1
80 | with:
81 | folder: dist
82 | exclude: |
83 | tmp.txt
84 | tmp/
85 | tmp2/*.txt
86 | tmp2/*/*.txt
87 | # match dist/tmp.txt
88 | # match dist/tmp/
89 | # match dist/tmp2/a.txt
90 | # match dist/tmp2/a/b.txt, not match dist/tmp2/tmp3/a/b.txt
91 | ```
92 | > 不支持`**`
93 |
94 | 或者
95 | ```yml
96 | - name: Clean files before upload
97 | run: rm -f dist/tmp.txt
98 | ```
99 |
100 | ## Docker image
101 | 直接使用已经build好的docker image
102 | ```yml
103 | - name: upload files to OSS
104 | uses: docker://fangbinwei/aliyun-oss-website-action:v1
105 | # 使用env而不是with, 参数可以见本项目的action.yml
106 | env:
107 | ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
108 | ACCESS_KEY_SECRET: ${{ secrets.ACCESS_KEY_SECRET }}
109 | BUCKET: your-bucket-name
110 | ENDPOINT: ali-oss-endpoint
111 | FOLDER: your-website-output-folder
112 | ```
113 |
114 | ## Demo
115 | ### 部署VuePress项目
116 |
117 | ```yml
118 |
119 | name: deploy vuepress
120 |
121 | on:
122 | push:
123 | branches:
124 | - master
125 |
126 | jobs:
127 | build:
128 |
129 | runs-on: ubuntu-latest
130 | steps:
131 | # load repo to /github/workspace
132 | - uses: actions/checkout@v2
133 | with:
134 | repository: fangbinwei/blog
135 | fetch-depth: 0
136 | - name: Use Node.js
137 | uses: actions/setup-node@v1
138 | with:
139 | node-version: '12'
140 | - run: npm install yarn@1.22.4 -g
141 | - run: yarn install
142 | # 打包文档命令
143 | - run: yarn docs:build
144 | - name: upload files to OSS
145 | uses: fangbinwei/aliyun-oss-website-action@v1
146 | with:
147 | accessKeyId: ${{ secrets.ACCESS_KEY_ID }}
148 | accessKeySecret: ${{ secrets.ACCESS_KEY_SECRET }}
149 | bucket: "your-bucket-name"
150 | endpoint: "oss-cn-shanghai.aliyuncs.com"
151 | folder: ".vuepress/dist"
152 | ```
153 | 具体可以参考本项目的[workflow](.github/workflows/test.yml), npm/yarn配合`action/cache`加速依赖安装
154 |
155 | ### Vue
156 |
157 | [see here](https://github.com/fangbinwei/oss-website-demo-spa-vue)
158 |
159 | ```yml
160 | - name: upload files to OSS
161 | uses: fangbinwei/aliyun-oss-website-action@v1
162 | with:
163 | accessKeyId: ${{ secrets.ACCESS_KEY_ID }}
164 | accessKeySecret: ${{ secrets.ACCESS_KEY_SECRET }}
165 | bucket: website-spa-vue-demo
166 | endpoint: oss-spa-demo.fangbinwei.cn
167 | cname: true
168 | folder: dist
169 | notFoundPage: index.html
170 | htmlCacheControl: no-cache
171 | imageCacheControl: max-age=864001
172 | otherCacheControl: max-age=2592001
173 | ```
174 |
175 | ## FAQ
176 |
177 | ### 配合CDN使用时, OSS更新后, CDN未刷新
178 |
179 | 开启OSS提供的CDN缓存自动刷新功能, 将触发操作配置为`PutObject`, `DeleteObject`.
180 |
181 | 
182 |
183 | 
184 |
185 | ### `endpoint`使用自定义域名, 但是无法上传
186 | 1. 如果`endpoint`的域名CNAME记录为阿里云CDN, CDN是否配置了http强制跳转https? 若配置了, 需要在`endpoint`中指定https, 即`endpoint`为`https://example.org`
187 |
188 | 2. 如果`endpoint`的域名CNAME记录为阿里云CDN, 在CDN为加速范围为全球时有遇到过如下报错`The bucket you are attempting to access must be addressed using the specified endpoint. Please send all future requests to this endpoint.`, 则`endpoint`不能使用自定义域名, 使用OSS源站的endpoint.
189 |
190 | ### 配置了environment secret怎么不生效
191 |
192 | 
193 |
194 | 如果使用environment secret, 那么需要如下类似的配置
195 |
196 | ```diff
197 |
198 | jobs:
199 | build:
200 | runs-on: ubuntu-latest
201 | + environment: your-environment-name
202 |
203 | ```
--------------------------------------------------------------------------------
/main_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "aliyun-oss-website-action/config"
5 | "aliyun-oss-website-action/operation"
6 | "aliyun-oss-website-action/utils"
7 | "fmt"
8 | "os"
9 | "testing"
10 |
11 | "github.com/fangbinwei/aliyun-oss-go-sdk/oss"
12 | "github.com/stretchr/testify/assert"
13 | )
14 |
15 | func testSetStaticWebsiteConfig(t *testing.T) {
16 | assert := assert.New(t)
17 |
18 | err := operation.SetStaticWebsiteConfig()
19 | assert.NoError(err)
20 | }
21 |
22 | func testUpload(t *testing.T) {
23 | assert := assert.New(t)
24 | fmt.Println("---- [delete] ---->")
25 | errs := operation.DeleteObjects(config.Bucket)
26 | fmt.Println("<---- [delete end] ----")
27 | assert.Equal(len(errs), 0)
28 |
29 | lor, err := config.Bucket.ListObjects()
30 | assert.NoError(err)
31 | assert.Equal(0, len(lor.Objects))
32 |
33 | records := utils.WalkDir(config.Folder)
34 |
35 | // overwrite, since dotenv doesn't support multiline
36 | config.Exclude = []string{"exclude.txt", "exclude/"}
37 | fmt.Println("---- [upload] ---->")
38 | uploaded, uploadErrs := operation.UploadObjects(config.Folder, config.Bucket, records, nil)
39 | fmt.Println("<---- [upload end] ----")
40 | assert.Equal(0, len(uploadErrs), uploadErrs)
41 |
42 | lor, err = config.Bucket.ListObjects()
43 | assert.NoError(err)
44 | assert.Equal(len(uploaded), len(lor.Objects))
45 |
46 | // test exclude
47 | lor, err = config.Bucket.ListObjects(oss.Prefix("exclude"))
48 | assert.NoError(err)
49 | assert.Empty(lor.Objects)
50 |
51 | // test cache-control
52 | for _, u := range uploaded {
53 | // 如果自定义域名解析到了cdn, 这个接口会报错, 但是上面的测试流程正常
54 | // 避开方法: env中endpoint使用bucket的endpoint或者bucket域名, 而不是自定义域名
55 | props, err := config.Bucket.GetObjectDetailedMeta(u.ObjectKey)
56 | assert.NoError(err)
57 | cacheControl := props.Get("Cache-Control")
58 | if utils.IsImage(u.ObjectKey) {
59 | assert.Equal(config.ImageCacheControl, cacheControl)
60 | } else if utils.IsHTML(u.ObjectKey) {
61 | assert.Equal(config.HTMLCacheControl, cacheControl)
62 | } else {
63 | assert.Equal(config.OtherCacheControl, cacheControl)
64 | }
65 | }
66 |
67 | }
68 |
69 | func testUploadIncrementalFirst(t *testing.T) {
70 | assert := assert.New(t)
71 |
72 | fmt.Println("---- [incremental] ---->")
73 | incremental, err := operation.GetRemoteIncrementalConfig(config.Bucket)
74 | assert.Error(err)
75 | assert.Empty(incremental)
76 | fmt.Println("<---- [incremental end] ----")
77 |
78 | if incremental == nil {
79 | fmt.Println("---- [delete] ---->")
80 | errs := operation.DeleteObjects(config.Bucket)
81 | fmt.Println("<---- [delete end] ----")
82 | assert.Equal(len(errs), 0)
83 | }
84 |
85 | lor, err := config.Bucket.ListObjects()
86 | assert.NoError(err)
87 | assert.Equal(0, len(lor.Objects))
88 |
89 | records := utils.WalkDir(config.Folder)
90 |
91 | // overwrite, since dotenv doesn't support multiline
92 | config.Exclude = []string{"exclude.txt", "exclude/"}
93 | fmt.Println("---- [upload] ---->")
94 | uploaded, uploadErrs := operation.UploadObjects(config.Folder, config.Bucket, records, incremental)
95 | fmt.Println("<---- [upload end] ----")
96 | assert.Equal(0, len(uploadErrs), uploadErrs)
97 |
98 | lor, err = config.Bucket.ListObjects()
99 | assert.NoError(err)
100 | assert.Equal(len(uploaded), len(lor.Objects))
101 |
102 | fmt.Println("---- [incremental] ---->")
103 | err = operation.UploadIncrementalConfig(config.Bucket, uploaded)
104 | fmt.Println("<---- [incremental end] ----")
105 | assert.NoError(err)
106 |
107 | incremental, err = operation.GetRemoteIncrementalConfig(config.Bucket)
108 | assert.NoError(err)
109 | assert.Equal(len(uploaded), len(incremental.M))
110 | for _, v := range uploaded {
111 | assert.False(v.Incremental)
112 | assert.Equal(v.ContentMD5, incremental.M[v.ObjectKey].ContentMD5)
113 | assert.Equal(v.CacheControl, incremental.M[v.ObjectKey].CacheControl)
114 | }
115 |
116 | // test exclude
117 | lor, err = config.Bucket.ListObjects(oss.Prefix("exclude"))
118 | assert.NoError(err)
119 | assert.Empty(lor.Objects)
120 |
121 | // test cache-control
122 | for _, u := range uploaded {
123 | // 如果自定义域名解析到了cdn, 这个接口会报错, 但是上面的测试流程正常
124 | // 避开方法: env中endpoint使用bucket的endpoint或者bucket域名, 而不是自定义域名
125 | props, err := config.Bucket.GetObjectDetailedMeta(u.ObjectKey)
126 | assert.NoError(err)
127 | cacheControl := props.Get("Cache-Control")
128 | if utils.IsImage(u.ObjectKey) {
129 | assert.Equal(config.ImageCacheControl, cacheControl)
130 | } else if utils.IsPDF(u.ObjectKey) {
131 | assert.Equal(config.PDFCacheControl, cacheControl)
132 | } else if utils.IsHTML(u.ObjectKey) {
133 | assert.Equal(config.HTMLCacheControl, cacheControl)
134 | } else {
135 | assert.Equal(config.OtherCacheControl, cacheControl)
136 | }
137 | }
138 | }
139 |
140 | func testUploadIncrementalSecond(t *testing.T) {
141 | assert := assert.New(t)
142 |
143 | fmt.Println("---- [incremental] ---->")
144 | incremental, err := operation.GetRemoteIncrementalConfig(config.Bucket)
145 | assert.NoError(err)
146 | fmt.Println("<---- [incremental end] ----")
147 |
148 | lor, err := config.Bucket.ListObjects()
149 | assert.NoError(err)
150 | assert.Equal(len(lor.Objects)-1, len(incremental.M))
151 |
152 | records := utils.WalkDir(config.Folder)
153 |
154 | fmt.Println("---- [upload] ---->")
155 | uploaded, uploadErrs := operation.UploadObjects(config.Folder, config.Bucket, records, incremental)
156 | fmt.Println("<---- [upload end] ----")
157 | assert.Equal(0, len(uploadErrs), uploadErrs)
158 |
159 | lor, err = config.Bucket.ListObjects()
160 |
161 | assert.NoError(err)
162 | assert.Equal(len(uploaded), len(lor.Objects)-1)
163 |
164 | // incremental.M中剩余的项是待删除的, 数量为0, 因为此次上传和上次上传的文件一模一样
165 | assert.Equal(0, len(incremental.M))
166 |
167 | fmt.Println("---- [delete] ---->")
168 | // 只删除.actioninfo
169 | errs := operation.DeleteObjectsIncremental(config.Bucket, incremental)
170 | fmt.Println("<---- [delete end] ----")
171 | assert.Equal(0, len(errs))
172 |
173 | lor, err = config.Bucket.ListObjects()
174 |
175 | assert.NoError(err)
176 | assert.Equal(len(uploaded), len(lor.Objects))
177 |
178 | fmt.Println("---- [incremental] ---->")
179 | err = operation.UploadIncrementalConfig(config.Bucket, uploaded)
180 | fmt.Println("<---- [incremental end] ----")
181 | assert.NoError(err)
182 |
183 | incremental, err = operation.GetRemoteIncrementalConfig(config.Bucket)
184 | assert.NoError(err)
185 | assert.Equal(len(uploaded), len(incremental.M))
186 | for _, v := range uploaded {
187 | // 全都不需要上传, 命中incremental
188 | assert.True(v.Incremental)
189 | assert.Equal(v.ContentMD5, incremental.M[v.ObjectKey].ContentMD5)
190 | assert.Equal(v.CacheControl, incremental.M[v.ObjectKey].CacheControl)
191 | }
192 |
193 | }
194 |
195 | func testUploadIncrementalThird(t *testing.T) {
196 | assert := assert.New(t)
197 | folder := "testdata/group2"
198 | // 改变cache-control会让对应文件重新上传, 即使hash没变
199 | config.ImageCacheControl = "no-cache"
200 |
201 | fmt.Println("---- [incremental] ---->")
202 | incremental, err := operation.GetRemoteIncrementalConfig(config.Bucket)
203 | assert.NoError(err)
204 | fmt.Println("<---- [incremental end] ----")
205 |
206 | lor, err := config.Bucket.ListObjects()
207 | assert.NoError(err)
208 | assert.Equal(len(lor.Objects)-1, len(incremental.M))
209 |
210 | records := utils.WalkDir(folder)
211 |
212 | fmt.Println("---- [upload] ---->")
213 | uploaded, uploadErrs := operation.UploadObjects(folder, config.Bucket, records, incremental)
214 | fmt.Println("<---- [upload end] ----")
215 | assert.Equal(0, len(uploadErrs), uploadErrs)
216 |
217 | // incremental.M中剩余的项是待删除的, 大于0
218 | assert.Greater(len(incremental.M), 0)
219 | for _, v := range uploaded {
220 | if v.ObjectKey == "empty.js" {
221 | assert.True(v.Incremental)
222 | continue
223 | }
224 | if v.ObjectKey == "example.js" {
225 | assert.False(v.Incremental)
226 | continue
227 | }
228 | if v.ObjectKey == "favicon.ico" {
229 | assert.False(v.Incremental)
230 | continue
231 | }
232 | t.Fail()
233 | }
234 |
235 | fmt.Println("---- [delete] ---->")
236 | errs := operation.DeleteObjectsIncremental(config.Bucket, incremental)
237 | fmt.Println("<---- [delete end] ----")
238 | assert.Equal(0, len(errs))
239 |
240 | lor, err = config.Bucket.ListObjects()
241 | assert.NoError(err)
242 | assert.Equal(len(uploaded), len(lor.Objects))
243 |
244 | fmt.Println("---- [incremental] ---->")
245 | err = operation.UploadIncrementalConfig(config.Bucket, uploaded)
246 | fmt.Println("<---- [incremental end] ----")
247 | assert.NoError(err)
248 |
249 | incremental, err = operation.GetRemoteIncrementalConfig(config.Bucket)
250 | assert.NoError(err)
251 | assert.Equal(len(uploaded), len(incremental.M))
252 | for _, v := range uploaded {
253 | assert.Equal(v.ContentMD5, incremental.M[v.ObjectKey].ContentMD5)
254 | assert.Equal(v.CacheControl, incremental.M[v.ObjectKey].CacheControl)
255 | }
256 |
257 | }
258 |
259 | func TestAction(t *testing.T) {
260 | t.Run("SetStaticWebsiteConfig", testSetStaticWebsiteConfig)
261 | t.Run("First upload", testUpload)
262 | t.Run("Second upload", testUpload)
263 | t.Run("First incremental upload without .actioninfo", testUploadIncrementalFirst)
264 | t.Run("Second incremental upload", testUploadIncrementalSecond)
265 | t.Run("Third incremental upload, change cache-control", testUploadIncrementalThird)
266 | }
267 |
268 | func TestMain(m *testing.M) {
269 | code := m.Run()
270 |
271 | fmt.Println("Empty bucket after test")
272 | operation.DeleteObjects(config.Bucket)
273 |
274 | os.Exit(code)
275 | }
276 |
--------------------------------------------------------------------------------