1
0
mirror of https://github.com/minio/mc.git synced 2025-07-30 07:23:03 +03:00

Remove/deprecate admin lock commands (#2503)

This commit is contained in:
Harshavardhana
2018-08-06 12:59:13 -07:00
committed by Dee Koder
parent 9998e077df
commit 26f5102eae
11 changed files with 25 additions and 590 deletions

View File

@ -1,153 +0,0 @@
/*
* Minio Client (C) 2016 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cmd
import (
"encoding/json"
"fmt"
"path/filepath"
"time"
"github.com/minio/cli"
"github.com/minio/mc/pkg/probe"
"github.com/minio/minio/pkg/madmin"
)
var (
adminLockClearFlags = []cli.Flag{
cli.StringFlag{
Name: "duration, d",
Usage: "Only clear locks held for longer than NN[h|m|s]",
Value: "0s",
},
cli.BoolFlag{
Name: "force",
Usage: "Force a clear lock operation",
},
}
)
var adminLockClearCmd = cli.Command{
Name: "clear",
Usage: "Clear locks held in a given Minio server",
Before: setGlobalsFromContext,
Action: mainAdminLockClear,
Flags: append(adminLockClearFlags, globalFlags...),
CustomHelpTemplate: `NAME:
{{.HelpName}} - {{.Usage}}
USAGE:
{{.HelpName}} [FLAGS] TARGET
FLAGS:
{{range .VisibleFlags}}{{.}}
{{end}}
EXAMPLES:
1. Clear all locks held on 'testbucket' in a Minio server with alias 'play'.
$ {{.HelpName}} --force play/testbucket
2. Clear all 'testbucket' locks older than 15 minutes.
$ {{.HelpName}} --force --duration 15m play/testbucket/
3. Clear all locks held on all objects under prefix 'dir'.
$ {{.HelpName}} --force play/testbucket/dir/
`,
}
// lockClearMessage container to hold locks information.
type lockClearMessage struct {
Status string `json:"status"`
madmin.VolumeLockInfo
}
// String colorized service status message.
func (u lockClearMessage) String() string {
msg := fmt.Sprintf("%s/%s (LocksOnObject: %d, locksAcquiredOnObject: %d, totalBlockLocks:%d): ",
u.Bucket,
u.Object,
u.LocksOnObject,
u.LocksAcquiredOnObject,
u.TotalBlockedLocks)
for _, detail := range u.LockDetailsOnObject {
msg += fmt.Sprintf(" %+v", detail)
}
msg += "\n"
return msg
}
// JSON jsonified service status Message message.
func (u lockClearMessage) JSON() string {
u.Status = "success"
statusJSONBytes, e := json.Marshal(u)
fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
return string(statusJSONBytes)
}
// checkAdminLockClearSyntax - validate all the passed arguments
func checkAdminLockClearSyntax(ctx *cli.Context) {
if len(ctx.Args()) == 0 || len(ctx.Args()) > 2 {
cli.ShowCommandHelpAndExit(ctx, "clear", 1)
}
// Check if a bucket is specified.
aliasedURL := filepath.ToSlash(ctx.Args().Get(0))
splits := splitStr(aliasedURL, "/", 3)
if splits[1] == "" {
fatalIf(errBucketNotSpecified().Trace(aliasedURL), "Cannot clear locks.")
}
if isForce := ctx.Bool("force"); isForce {
return
}
fatalIf(errDummy().Trace(),
"Clearing locks requires --force flag. This operation is "+
"*IRREVERSIBLE*. Please review carefully before"+
" performing this *DANGEROUS* operation.")
}
func mainAdminLockClear(ctx *cli.Context) error {
checkAdminLockClearSyntax(ctx)
// Get the alias parameter from cli
args := ctx.Args()
aliasedURL := args.Get(0)
// Parse duration flag
duration, e := time.ParseDuration(ctx.String("duration"))
fatalIf(probe.NewError(e), "Unable to parse the passed duration flag.")
// Create a new Minio Admin Client
client, err := newAdminClient(aliasedURL)
fatalIf(err, "Cannot get a configured admin connection.")
aliasedURL = filepath.ToSlash(aliasedURL)
splits := splitStr(aliasedURL, "/", 3)
// Clear locks related to a specified pair of bucket and prefix
locksInfo, e := client.ClearLocks(splits[1], splits[2], duration)
fatalIf(probe.NewError(e), "Cannot clear the specified locks.")
for _, l := range locksInfo {
printMsg(lockClearMessage{VolumeLockInfo: l})
}
return nil
}

View File

@ -1,140 +0,0 @@
/*
* Minio Client (C) 2016 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cmd
import (
"encoding/json"
"fmt"
"path/filepath"
"time"
"github.com/minio/cli"
"github.com/minio/mc/pkg/probe"
"github.com/minio/minio/pkg/madmin"
)
var (
adminLockListFlags = []cli.Flag{
cli.StringFlag{
Name: "duration, d",
Usage: "Only show locks that are held for longer than NN[h|m|s]",
Value: "24h",
},
}
)
var adminLockListCmd = cli.Command{
Name: "list",
Usage: "List locks held in a given Minio server",
Action: mainAdminLockList,
Before: setGlobalsFromContext,
Flags: append(adminLockListFlags, globalFlags...),
CustomHelpTemplate: `NAME:
{{.HelpName}} - {{.Usage}}
USAGE:
{{.HelpName}} [FLAGS] TARGET
FLAGS:
{{range .VisibleFlags}}{{.}}
{{end}}
EXAMPLES:
1. List locks held on 'testbucket' in a Minio server with alias 'play'.
$ {{.HelpName}} play/testbucket/
2. List locks held on 'testbucket' for more than 15 minutes.
$ {{.HelpName}} --duration 15m play/testbucket/
3. List locks held on all objects under prefix 'dir'.
$ {{.HelpName}} play/testbucket/dir/
`,
}
// lockListMessage container to hold locks information.
type lockListMessage struct {
Status string `json:"status"`
madmin.VolumeLockInfo
}
// String colorized service status message.
func (u lockListMessage) String() string {
msg := fmt.Sprintf("%s/%s (LocksOnObject: %d, locksAcquiredOnObject: %d, totalBlockLocks:%d): ",
u.Bucket,
u.Object,
u.LocksOnObject,
u.LocksAcquiredOnObject,
u.TotalBlockedLocks)
for _, detail := range u.LockDetailsOnObject {
msg += fmt.Sprintf(" %+v", detail)
}
msg += "\n"
return msg
}
// JSON jsonified service status Message message.
func (u lockListMessage) JSON() string {
u.Status = "success"
statusJSONBytes, e := json.Marshal(u)
fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
return string(statusJSONBytes)
}
// checkAdminLockListSyntax - validate all the passed arguments
func checkAdminLockListSyntax(ctx *cli.Context) {
if len(ctx.Args()) == 0 || len(ctx.Args()) > 2 {
cli.ShowCommandHelpAndExit(ctx, "list", 1) // last argument is exit code
}
// Check if a bucket is specified.
aliasedURL := filepath.ToSlash(ctx.Args().Get(0))
splits := splitStr(aliasedURL, "/", 3)
if splits[1] == "" {
fatalIf(errBucketNotSpecified().Trace(aliasedURL), "Cannot list locks.")
}
}
func mainAdminLockList(ctx *cli.Context) error {
checkAdminLockListSyntax(ctx)
// Get the alias parameter from cli
args := ctx.Args()
aliasedURL := args.Get(0)
// Parse duration flag
duration, e := time.ParseDuration(ctx.String("duration"))
fatalIf(probe.NewError(e), "Unable to parse the passed duration flag.")
// Create a new Minio Admin Client
client, err := newAdminClient(aliasedURL)
fatalIf(err, "Cannot get a configured admin connection.")
aliasedURL = filepath.ToSlash(aliasedURL)
splits := splitStr(aliasedURL, "/", 3)
// Fetch the lock info related to a specified pair of bucket and prefix
locksInfo, e := client.ListLocks(splits[1], splits[2], duration)
fatalIf(probe.NewError(e), "Cannot get lock status.")
for _, l := range locksInfo {
printMsg(lockListMessage{VolumeLockInfo: l})
}
return nil
}

View File

@ -1,43 +0,0 @@
/*
* Minio Client (C) 2016 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cmd
import "github.com/minio/cli"
var (
adminLockFlags = []cli.Flag{}
)
var adminLockCmd = cli.Command{
Name: "lock",
Usage: "Control locks in servers",
Action: mainAdminLock,
Before: setGlobalsFromContext,
Flags: append(adminLockFlags, globalFlags...),
Subcommands: []cli.Command{
adminLockListCmd,
adminLockClearCmd,
},
HideHelpCommand: true,
}
// mainAdminLock is the handle for "mc admin lock" command.
func mainAdminLock(ctx *cli.Context) error {
cli.ShowCommandHelp(ctx, ctx.Args().First())
return nil
// Sub-commands like "list", "unlock" have their own main.
}

View File

@ -34,7 +34,6 @@ var adminCmd = cli.Command{
adminInfoCmd, adminInfoCmd,
adminCredsCmd, adminCredsCmd,
adminConfigCmd, adminConfigCmd,
adminLockCmd,
adminHealCmd, adminHealCmd,
}, },
} }

View File

@ -116,10 +116,3 @@ var errSourceTargetSame = func(URL string) *probe.Error {
msg := "Source and target URL can not be same : " + URL msg := "Source and target URL can not be same : " + URL
return probe.NewError(sourceTargetSameErr(errors.New(msg))).Untrace() return probe.NewError(sourceTargetSameErr(errors.New(msg))).Untrace()
} }
type bucketNotSpecifiedErr error
var errBucketNotSpecified = func() *probe.Error {
msg := "This operation requires a " + "bucket to be specified."
return probe.NewError(bucketNotSpecifiedErr(errors.New(msg))).Untrace()
}

View File

@ -36,10 +36,10 @@ func main() {
``` ```
| Service operations | Info operations | LockInfo operations | Healing operations | Config operations | Misc | | Service operations | Info operations | Healing operations | Config operations | Misc |
|:------------------------------------|:----------------------------|:----------------------------|:--------------------------------------|:--------------------------|:------------------------------------| |:------------------------------------|:----------------------------|:----------------------------|:--------------------------------------|:--------------------------|:------------------------------------|
| [`ServiceStatus`](#ServiceStatus) | [`ServerInfo`](#ServerInfo) | [`ListLocks`](#ListLocks) | [`Heal`](#Heal) | [`GetConfig`](#GetConfig) | [`SetCredentials`](#SetCredentials) | | [`ServiceStatus`](#ServiceStatus) | [`ServerInfo`](#ServerInfo) | [`Heal`](#Heal) | [`GetConfig`](#GetConfig) | [`SetCredentials`](#SetCredentials) |
| [`ServiceSendAction`](#ServiceSendAction) | | [`ClearLocks`](#ClearLocks) | | [`SetConfig`](#SetConfig) | | | [`ServiceSendAction`](#ServiceSendAction) | | | [`SetConfig`](#SetConfig) | |
## 1. Constructor ## 1. Constructor
@ -203,38 +203,6 @@ Fetches information for all cluster nodes, such as server properties, storage in
``` ```
## 5. Lock operations
<a name="ListLocks"></a>
### ListLocks(bucket, prefix string, duration time.Duration) ([]VolumeLockInfo, error)
If successful returns information on the list of locks held on ``bucket`` matching ``prefix`` for longer than ``duration`` seconds.
__Example__
``` go
volLocks, err := madmClnt.ListLocks("mybucket", "myprefix", 30 * time.Second)
if err != nil {
log.Fatalln(err)
}
log.Println("List of locks: ", volLocks)
```
<a name="ClearLocks"></a>
### ClearLocks(bucket, prefix string, duration time.Duration) ([]VolumeLockInfo, error)
If successful returns information on the list of locks cleared on ``bucket`` matching ``prefix`` for longer than ``duration`` seconds.
__Example__
``` go
volLocks, err := madmClnt.ClearLocks("mybucket", "myprefix", 30 * time.Second)
if err != nil {
log.Fatalln(err)
}
log.Println("List of locks cleared: ", volLocks)
```
## 6. Heal operations ## 6. Heal operations
<a name="Heal"></a> <a name="Heal"></a>
@ -243,25 +211,26 @@ __Example__
Start a heal sequence that scans data under given (possible empty) Start a heal sequence that scans data under given (possible empty)
`bucket` and `prefix`. The `recursive` bool turns on recursive `bucket` and `prefix`. The `recursive` bool turns on recursive
traversal under the given path. `dryRun` does not mutate on-disk data, traversal under the given path. `dryRun` does not mutate on-disk data,
but performs data validation. `incomplete` enables healing of but performs data validation.
multipart uploads that are in progress. `removeBadFiles` removes
unrecoverable files. `statisticsOnly` turns off detailed
heal-operations reporting in the status call.
Two heal sequences on overlapping paths may not be initiated. Two heal sequences on overlapping paths may not be initiated.
The progress of a heal should be followed using the `HealStatus` The progress of a heal should be followed using the same API `Heal`
by providing the `clientToken` previously obtained from a `Heal`
API. The server accumulates results of the heal traversal and waits API. The server accumulates results of the heal traversal and waits
for the client to receive and acknowledge them using the status for the client to receive and acknowledge them using the status
API. When the statistics-only option is set, the server only maintains request by providing `clientToken`.
aggregates statistics - in this case, no acknowledgement of results is
required.
__Example__ __Example__
``` go ``` go
healPath, err := madmClnt.HealStart("", "", true, false, true, false, false) opts := madmin.HealOpts{
Recursive: true,
DryRun: false,
}
forceStart := false
healPath, err := madmClnt.Heal("", "", opts, "", forceStart)
if err != nil { if err != nil {
log.Fatalln(err) log.Fatalln(err)
} }
@ -269,6 +238,14 @@ __Example__
``` ```
#### HealStartSuccess structure
| Param | Type | Description |
|----|--------|--------|
| s.ClientToken | _string_ | A unique token for a successfully started heal operation, this token is used to request realtime progress of the heal operation. |
| s.ClientAddress | _string_ | Address of the client which initiated the heal operation, the client address has the form "host:port".|
| s.StartTime | _time.Time_ | Time when heal was initially started.|
#### HealTaskStatus structure #### HealTaskStatus structure
| Param | Type | Description | | Param | Type | Description |
@ -277,7 +254,6 @@ __Example__
| s.FailureDetail | _string_ | Error message in case of heal sequence failure | | s.FailureDetail | _string_ | Error message in case of heal sequence failure |
| s.HealSettings | _HealOpts_ | Contains the booleans set in the `HealStart` call | | s.HealSettings | _HealOpts_ | Contains the booleans set in the `HealStart` call |
| s.Items | _[]HealResultItem_ | Heal records for actions performed by server | | s.Items | _[]HealResultItem_ | Heal records for actions performed by server |
| s.Statistics | _HealStatistics_ | Aggregate of heal records from beginning |
#### HealResultItem structure #### HealResultItem structure
@ -291,38 +267,6 @@ __Example__
| DiskInfo.AvailableOn | _[]int_ | List of disks on which the healed entity is present and healthy | | DiskInfo.AvailableOn | _[]int_ | List of disks on which the healed entity is present and healthy |
| DiskInfo.HealedOn | _[]int_ | List of disks on which the healed entity was restored | | DiskInfo.HealedOn | _[]int_ | List of disks on which the healed entity was restored |
#### HealStatistics structure
Most parameters represent the aggregation of heal operations since the
start of the heal sequence.
| Param | Type | Description |
|-------|-----|----------|
| NumDisks | _int_ | Number of disks configured in the backend |
| NumBucketsScanned | _int64_ | Number of buckets scanned |
| BucketsMissingByDisk | _map[int]int64_ | Map of disk to number of buckets missing |
| BucketsAvailableByDisk | _map[int]int64_ | Map of disk to number of buckets available |
| BucketsHealedByDisk | _map[int]int64_ | Map of disk to number of buckets healed on |
| NumObjectsScanned | _int64_ | Number of objects scanned |
| NumUploadsScanned | _int64_ | Number of uploads scanned |
| ObjectsByAvailablePC | _map[int64]_ | Map of available part counts (after heal) to number of objects |
| ObjectsByHealedPC | _map[int64]_ | Map of healed part counts to number of objects |
| ObjectsMissingByDisk | _map[int64]_ | Map of disk number to number of objects with parts missing on that disk |
| ObjectsAvailableByDisk | _map[int64]_ | Map of disk number to number of objects available on that disk |
| ObjectsHealedByDisk | _map[int64]_ | Map of disk number to number of objects healed on that disk |
__Example__
``` go
res, err := madmClnt.HealStatus("", "")
if err != nil {
log.Fatalln(err)
}
log.Printf("Heal sequence status data %#v", res)
```
## 7. Config operations ## 7. Config operations
<a name="GetConfig"></a> <a name="GetConfig"></a>

View File

@ -62,7 +62,7 @@ func (adm *AdminClient) GetConfig() ([]byte, error) {
return nil, httpRespToErrorResponse(resp) return nil, httpRespToErrorResponse(resp)
} }
// Return the JSON marshalled bytes to user. // Return the JSON marshaled bytes to user.
return ioutil.ReadAll(resp.Body) return ioutil.ReadAll(resp.Body)
} }

View File

@ -1,22 +0,0 @@
/*
* Minio Cloud Storage, (C) 2016 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package madmin
const (
// Unsigned payload.
unsignedPayload = "UNSIGNED-PAYLOAD"
)

View File

@ -1,135 +0,0 @@
/*
* Minio Cloud Storage, (C) 2016 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package madmin
import (
"encoding/json"
"io"
"io/ioutil"
"net/http"
"net/url"
"time"
)
type statusType string
type lockType string
// OpsLockState - represents lock specific details.
type OpsLockState struct {
OperationID string `json:"id"` // String containing operation ID.
LockSource string `json:"source"` // Operation type (GetObject, PutObject...)
LockType lockType `json:"type"` // Lock type (RLock, WLock)
Status statusType `json:"status"` // Status can be Running/Ready/Blocked.
Since time.Time `json:"since"` // Time when the lock was initially held.
}
// VolumeLockInfo - represents summary and individual lock details of all
// locks held on a given bucket, object.
type VolumeLockInfo struct {
Bucket string `json:"bucket"`
Object string `json:"object"`
// All locks blocked + running for given <volume,path> pair.
LocksOnObject int64 `json:"-"`
// Count of operations which has successfully acquired the lock
// but hasn't unlocked yet( operation in progress).
LocksAcquiredOnObject int64 `json:"-"`
// Count of operations which are blocked waiting for the lock
// to be released.
TotalBlockedLocks int64 `json:"-"`
// Count of all read locks
TotalReadLocks int64 `json:"readLocks"`
// Count of all write locks
TotalWriteLocks int64 `json:"writeLocks"`
// State information containing state of the locks for all operations
// on given <volume,path> pair.
LockDetailsOnObject []OpsLockState `json:"lockOwners"`
}
// getLockInfos - unmarshal []VolumeLockInfo from a reader.
func getLockInfos(body io.Reader) ([]VolumeLockInfo, error) {
respBytes, err := ioutil.ReadAll(body)
if err != nil {
return nil, err
}
var lockInfos []VolumeLockInfo
err = json.Unmarshal(respBytes, &lockInfos)
if err != nil {
return nil, err
}
return lockInfos, nil
}
// ListLocks - Calls List Locks Management API to fetch locks matching
// bucket, prefix and held before the duration supplied.
func (adm *AdminClient) ListLocks(bucket, prefix string,
duration time.Duration) ([]VolumeLockInfo, error) {
queryVal := make(url.Values)
queryVal.Set("bucket", bucket)
queryVal.Set("prefix", prefix)
queryVal.Set("older-than", duration.String())
// Execute GET on /minio/admin/v1/locks to list locks.
resp, err := adm.executeMethod("GET", requestData{
queryValues: queryVal,
relPath: "/v1/locks",
})
defer closeResponse(resp)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, httpRespToErrorResponse(resp)
}
return getLockInfos(resp.Body)
}
// ClearLocks - Calls Clear Locks Management API to clear locks held
// on bucket, matching prefix older than duration supplied.
func (adm *AdminClient) ClearLocks(bucket, prefix string,
duration time.Duration) ([]VolumeLockInfo, error) {
queryVal := make(url.Values)
queryVal.Set("bucket", bucket)
queryVal.Set("prefix", prefix)
queryVal.Set("duration", duration.String())
// Execute POST on /?lock to clear locks.
resp, err := adm.executeMethod("DELETE", requestData{
queryValues: queryVal,
relPath: "/v1/locks",
})
defer closeResponse(resp)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, httpRespToErrorResponse(resp)
}
return getLockInfos(resp.Body)
}

View File

@ -17,7 +17,6 @@
package madmin package madmin
import ( import (
"crypto/md5"
"encoding/json" "encoding/json"
"io" "io"
"io/ioutil" "io/ioutil"
@ -38,13 +37,6 @@ func sum256(data []byte) []byte {
return hash.Sum(nil) return hash.Sum(nil)
} }
// sumMD5 calculate sumMD5 sum for an input byte array.
func sumMD5(data []byte) []byte {
hash := md5.New()
hash.Write(data)
return hash.Sum(nil)
}
// jsonDecoder decode json to go type. // jsonDecoder decode json to go type.
func jsonDecoder(body io.Reader, v interface{}) error { func jsonDecoder(body io.Reader, v interface{}) error {
d := json.NewDecoder(body) d := json.NewDecoder(body)

6
vendor/vendor.json vendored
View File

@ -134,10 +134,10 @@
"revisionTime": "2018-07-11T12:25:12Z" "revisionTime": "2018-07-11T12:25:12Z"
}, },
{ {
"checksumSHA1": "MEC+K9aTG+8tfPjnJ4qj2Y+kc4s=", "checksumSHA1": "FIs3tgGerLUN+S9IewYzFhJtQQY=",
"path": "github.com/minio/minio/pkg/madmin", "path": "github.com/minio/minio/pkg/madmin",
"revision": "3dc13323e51dc7038232f5f02f55b37b388c59c2", "revision": "556a51120ce8a68c6745ab0b0e374390529479a8",
"revisionTime": "2018-05-16T01:20:22Z" "revisionTime": "2018-08-02T17:39:42Z"
}, },
{ {
"checksumSHA1": "flg07CqTxM9togozKRQiJugao4s=", "checksumSHA1": "flg07CqTxM9togozKRQiJugao4s=",