blob: 0aeb0f289da89aa928ef0652f9e6236bf9855c7a [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 utils
import (
"archive/zip"
"io"
"os"
"path/filepath"
"strings"
"github.com/apache/openwhisk-wskdeploy/wski18n"
"github.com/apache/openwhisk-wskdeploy/wskprint"
)
const PATH_WILDCARD = "*"
const ONE_DIR_UP = "../"
func NewZipWriter(src string, des string, include [][]string, exclude []string, manifestFilePath string) *ZipWriter {
zw := &ZipWriter{
src: src,
des: des,
include: include,
exclude: exclude,
excludedFiles: make(map[string]bool, 0),
manifestFilePath: manifestFilePath,
}
return zw
}
type ZipWriter struct {
src string
des string
include [][]string
exclude []string
excludedFiles map[string]bool
manifestFilePath string
zipWriter *zip.Writer
}
type Include struct {
source string
destination string
}
func (zw *ZipWriter) zipFile(path string, f os.FileInfo, err error) error {
var file *os.File
var wr io.Writer
var verboseMsg string
if err != nil {
return err
}
if zw.excludedFiles[filepath.Clean(path)] {
verboseMsg = wski18n.T(wski18n.ID_VERBOSE_ZIP_EXCLUDING_FILE_X_path_X,
map[string]interface{}{
wski18n.KEY_PATH: path,
})
wskprint.PrintlnOpenWhiskVerbose(Flags.Verbose, verboseMsg)
return nil
}
if !f.Mode().IsRegular() || f.Size() == 0 {
return nil
}
if file, err = os.Open(path); err != nil {
return err
}
defer file.Close()
fileName := strings.TrimPrefix(path, zw.src+"/")
if wr, err = zw.zipWriter.Create(fileName); err != nil {
return err
}
if _, err = io.Copy(wr, file); err != nil {
return err
}
verboseMsg = wski18n.T(wski18n.ID_VERBOSE_ZIP_ADDING_FILE_X_path_X,
map[string]interface{}{
wski18n.KEY_PATH: path,
})
wskprint.PrintlnOpenWhiskVerbose(Flags.Verbose, verboseMsg)
return nil
}
func (zw *ZipWriter) buildIncludeMetadata() ([]Include, error) {
var includeInfo []Include
var listOfSourceFiles []string
var err error
var verboseMsg string
// iterate over set of included files specified in manifest YAML e.g.
// include:
// - ["source"]
// - ["source", "destination"]
for _, includeData := range zw.include {
var i Include
// if "destination" is not specified, its considered same as "source"
// "source" is relative to where manifest.yaml file is located
// relative source path is converted to absolute path by appending manifest path
// since the relative source path might not be accessible from where wskdeploy is invoked
// "destination" is relative to the action directory, the one specified in function
// relative path is converted to absolute path by appending function directory
if len(includeData) == 1 {
i.source = filepath.Join(zw.manifestFilePath, includeData[0])
i.destination = filepath.Join(zw.src, includeData[0])
verboseMsg = wski18n.T(wski18n.ID_VERBOSE_ZIP_INCLUDE_SOURCE_PATH_X_path_X,
map[string]interface{}{
wski18n.KEY_PATH: includeData[0],
})
wskprint.PrintlnOpenWhiskVerbose(Flags.Verbose, verboseMsg)
} else if len(includeData) == 2 {
i.source = filepath.Join(zw.manifestFilePath, includeData[0])
i.destination = zw.src + "/" + includeData[1]
verboseMsg = wski18n.T(wski18n.ID_VERBOSE_ZIP_INCLUDE_SOURCE_PATH_X_path_X_DESTINATION_PATH_X_dest_X,
map[string]interface{}{
wski18n.KEY_PATH: includeData[0],
wski18n.KEY_DESTINATION: includeData[1],
})
} else {
if len(includeData) == 0 {
verboseMsg = wski18n.T(wski18n.ID_VERBOSE_INVALID_INCLUDE_ENTRY,
map[string]interface{}{
wski18n.KEY_INCLUDE: "",
})
wskprint.PrintlnOpenWhiskVerbose(Flags.Verbose, verboseMsg)
} else {
for index, d := range includeData {
includeData[index] = "\"" + d + "\""
}
includeEntry := strings.Join(includeData, ", ")
verboseMsg = wski18n.T(wski18n.ID_VERBOSE_INVALID_INCLUDE_ENTRY,
map[string]interface{}{
wski18n.KEY_INCLUDE: includeEntry,
})
wskprint.PrintlnOpenWhiskVerbose(Flags.Verbose, verboseMsg)
}
continue
}
// set destDir to the destination location
// check if its a file than change it to the Dir of destination file
destDir := i.destination
if isFilePath(destDir) {
destDir = filepath.Dir(destDir)
}
// trim path wildcard "*" from the destination path as if it has any
destDirs := strings.Split(destDir, PATH_WILDCARD)
destDir = destDirs[0]
// retrieve the name of all files matching pattern or nil if there is no matching file
// listOfSourceFiles will hold a list of files matching patterns such as
// actions/* or actions/libs/* or actions/libs/*/utils.js or actions/*/*/utils.js
if listOfSourceFiles, err = filepath.Glob(i.source); err != nil {
return includeInfo, err
}
// handle the scenarios where included path is something similar to actions/common/*.js
// or actions/libs/* or actions/libs/*/utils.js
// and destination is set to libs/ or libs/* or ./libs/* or libs/*/utils.js or libs/ or ./libs/
if strings.ContainsAny(i.source, PATH_WILDCARD) {
wskprint.PrintlnOpenWhiskVerbose(Flags.Verbose, wski18n.T(wski18n.ID_VERBOSE_LIST_OF_FILES_MATCHING_PATTERN))
for _, file := range listOfSourceFiles {
var relPath string
if relPath, err = filepath.Rel(i.source, file); err != nil {
return includeInfo, err
}
relPath = strings.TrimLeft(relPath, ONE_DIR_UP)
j := Include{
source: file,
destination: filepath.Join(destDir, relPath),
}
includeInfo = append(includeInfo, j)
zw.excludedFiles[j.source] = false
verboseMsg = wski18n.T(wski18n.ID_VERBOSE_ZIP_INCLUDE_SOURCE_PATH_X_path_X_DESTINATION_PATH_X_dest_X,
map[string]interface{}{
wski18n.KEY_PATH: j.source,
wski18n.KEY_DESTINATION: j.destination,
})
wskprint.PrintlnOpenWhiskVerbose(Flags.Verbose, verboseMsg)
}
// handle scenarios where included path is something similar to actions/common/utils.js
// and destination is set to ./common/ i.e. no file name specified in the destination
} else {
if f, err := isFile(i.source); err == nil && f {
if _, file := filepath.Split(i.destination); len(file) == 0 {
_, sFile := filepath.Split(i.source)
i.destination = i.destination + sFile
}
}
// append just parsed include info to the list for further processing
wskprint.PrintlnOpenWhiskVerbose(Flags.Verbose, wski18n.T(wski18n.ID_VERBOSE_LIST_OF_FILES_MATCHING_PATTERN))
verboseMsg = wski18n.T(wski18n.ID_VERBOSE_ZIP_INCLUDE_SOURCE_PATH_X_path_X_DESTINATION_PATH_X_dest_X,
map[string]interface{}{
wski18n.KEY_PATH: i.source,
wski18n.KEY_DESTINATION: i.destination,
})
wskprint.PrintlnOpenWhiskVerbose(Flags.Verbose, verboseMsg)
includeInfo = append(includeInfo, i)
zw.excludedFiles[i.source] = false
}
}
return includeInfo, nil
}
func (zw *ZipWriter) buildExcludeMetadata() error {
var err error
for _, exclude := range zw.exclude {
exclude = filepath.Join(zw.manifestFilePath, exclude)
if err = zw.findExcludedIncludedFiles(exclude, true); err != nil {
return err
}
}
return err
}
func (zw *ZipWriter) findExcludedIncludedFiles(functionPath string, flag bool) error {
var err error
var files []string
var excludedFiles []string
var f bool
if !strings.HasSuffix(functionPath, PATH_WILDCARD) {
functionPath = filepath.Join(functionPath, PATH_WILDCARD)
}
if excludedFiles, err = filepath.Glob(functionPath); err != nil {
return err
}
for _, file := range excludedFiles {
err = filepath.Walk(file, func(path string, info os.FileInfo, err error) error {
files = append(files, path)
return nil
})
if err != nil {
return err
}
}
for _, file := range files {
if f, err = isFile(file); err != nil {
return err
} else if f {
zw.excludedFiles[file] = flag
} else {
if err = zw.findExcludedIncludedFiles(file, flag); err != nil {
return err
}
}
}
return err
}
func (zw *ZipWriter) Zip() error {
var zipFile *os.File
var err error
var fileInfo os.FileInfo
var verboseMsg string
// create zip file e.g. greeting.zip
if zipFile, err = os.Create(zw.des); err != nil {
return err
}
defer zipFile.Close()
verboseMsg = wski18n.T(wski18n.ID_VERBOSE_CREATING_ZIP_FILE_X_path_X,
map[string]interface{}{
wski18n.KEY_PATH: zipFile.Name(),
})
wskprint.PrintlnOpenWhiskVerbose(Flags.Verbose, verboseMsg)
// creating a new zip writter for greeting.zip
zw.zipWriter = zip.NewWriter(zipFile)
// build a map of file names and bool indicating whether the file is included or excluded
// iterate over the directory specified in "function", find the list of files and mark them as not excluded
if err = zw.findExcludedIncludedFiles(zw.src, false); err != nil {
return err
}
if err = zw.buildExcludeMetadata(); err != nil {
return err
}
// walk file system rooted at the directory specified in "function"
// walk over each file and dir under root directory e.g. function: actions/greeting
// add actions/greeting/index.js and actions/greeting/package.json to zip file
if err = filepath.Walk(zw.src, zw.zipFile); err != nil {
return nil
}
// maintain a list of included files and/or directories with their destination
var includeInfo []Include
includeInfo, err = zw.buildIncludeMetadata()
if err != nil {
return err
}
for _, i := range includeInfo {
if i.source != i.destination {
// now determine whether the included item is file or dir
// it could list something like this as well, "actions/common/*.js"
if fileInfo, err = os.Stat(i.source); err != nil {
return err
}
// if the included item is a directory, call a function to copy the
// entire directory recursively including its subdirectories and files
if fileInfo.Mode().IsDir() {
if err = copyDir(i.source, i.destination); err != nil {
return err
}
// if the included item is a file, call a function to copy the file
// along with its path by creating the parent directories
} else if fileInfo.Mode().IsRegular() {
if err = copyFile(i.source, i.destination); err != nil {
return err
}
}
}
// add included item into zip file greeting.zip
if err = filepath.Walk(i.destination, zw.zipFile); err != nil {
return nil
}
}
// now close the zip file greeting.zip as all the included items
// are added into the zip file along with the action root dir
if err = zw.zipWriter.Close(); err != nil {
return err
}
// and its safe to delete the files/directories which we copied earlier
// to include them in the zip file greeting.zip
for _, i := range includeInfo {
if filepath.Clean(i.source) != filepath.Clean(i.destination) {
verboseMsg = wski18n.T(wski18n.ID_VERBOSE_DELETING_FILE_X_path_X,
map[string]interface{}{
wski18n.KEY_PATH: i.destination,
})
wskprint.PrintlnOpenWhiskVerbose(Flags.Verbose, verboseMsg)
os.RemoveAll(i.destination)
}
}
return nil
}