<commit_before>package main

import (
    "fmt"
    "errors"
    "bytes"
    "io/ioutil"
    "bufio"
    "os"
    "strings"
    "regexp"
    flags "github.com/jessevdk/go-flags"
    "github.com/remeh/sizedwaitgroup"
)

const (
    Author  = "webdevops.io"
    Version = "1.1.1"
)

type changeset struct {
    SearchPlain string
    Search      *regexp.Regexp
    Replace     string
    MatchFound  bool
}

type changeresult struct {
    File   fileitem
    Output string
    Status bool
    Error  error
}

type fileitem struct {
    Path        string
    Output      string
}

var opts struct {
    ThreadCount             int      `           long:"threads"                       description:"Set thread concurrency for replacing in multiple files at same time" default:"20"`
    Mode                    string   `short:"m"  long:"mode"                          description:"replacement mode - replace: replace match with term; line: replace line with term; lineinfile: replace line with term or if not found append to term to file; template: parse content as golang template, search value have to start uppercase" default:"replace" choice:"replace" choice:"line" choice:"lineinfile" choice:"template"`
    ModeIsReplaceMatch      bool
    ModeIsReplaceLine       bool
    ModeIsLineInFile        bool
    ModeIsTemplate          bool
    Search                  []string `short:"s"  long:"search"                        description:"search term"`
    Replace                 []string `short:"r"  long:"replace"                       description:"replacement term"`
    LineinfileBefore        string   `           long:"lineinfile-before"             description:"add line before this regex"`
    LineinfileAfter         string   `           long:"lineinfile-after"              description:"add line after this regex"`
    CaseInsensitive         bool     `short:"i"  long:"case-insensitive"              description:"ignore case of pattern to match upper and lowercase characters"`
    Stdin                   bool     `           long:"stdin"                         description:"process stdin as input"`
    Output                  string   `short:"o"  long:"output"                        description:"write changes to this file (in one file mode)"`
    OutputStripFileExt      string   `           long:"output-strip-ext"              description:"strip file extension from written files (also available in multi file mode)"`
    Once                    string   `           long:"once"                          description:"replace search term only one in a file, keep duplicaes (keep, default) or remove them (unique)" optional:"true" optional-value:"keep" choice:"keep" choice:"unique"`
    Regex                   bool     `           long:"regex"                         description:"treat pattern as regex"`
    RegexBackref            bool     `           long:"regex-backrefs"                description:"enable backreferences in replace term"`
    RegexPosix              bool     `           long:"regex-posix"                   description:"parse regex term as POSIX regex"`
    Path                    string   `           long:"path"                          description:"use files in this path"`
    PathPattern             string   `           long:"path-pattern"                  description:"file pattern (* for wildcard, only basename of file)"`
    PathRegex               string   `           long:"path-regex"                    description:"file pattern (regex, full path)"`
    IgnoreEmpty             bool     `           long:"ignore-empty"                  description:"ignore empty file list, otherwise this will result in an error"`
    Verbose                 bool     `short:"v"  long:"verbose"                       description:"verbose mode"`
    DryRun                  bool     `           long:"dry-run"                       description:"dry run mode"`
    ShowVersion             bool     `short:"V"  long:"version"                       description:"show version and exit"`
    ShowOnlyVersion         bool     `           long:"dumpversion"                   description:"show only version number and exit"`
    ShowHelp                bool     `short:"h"  long:"help"                          description:"show this help message"`
}

var pathFilterDirectories = []string{"autom4te.cache", "blib", "_build", ".bzr", ".cdv", "cover_db", "CVS", "_darcs", "~.dep", "~.dot", ".git", ".hg", "~.nib", ".pc", "~.plst", "RCS", "SCCS", "_sgbak", ".svn", "_obj", ".idea"}

// Apply changesets to file
func applyChangesetsToFile(fileitem fileitem, changesets []changeset) (string, bool, error) {
    var (
        err error = nil
        output string = ""
        status bool = true
    )

    // try open file
    file, err := os.Open(fileitem.Path)
    if err != nil {
        return output, false, err
    }

    writeBufferToFile := false
    var buffer bytes.Buffer

    r := bufio.NewReader(file)
    line, e := Readln(r)
    for e == nil {
        newLine, lineChanged, skipLine := applyChangesetsToLine(line, changesets)

        if lineChanged || skipLine {
            writeBufferToFile = true
        }

        if !skipLine {
            buffer.WriteString(newLine + "\n")
        }

        line, e = Readln(r)
    }
    file.Close()

    // --mode=lineinfile
    if opts.ModeIsLineInFile {
        lifBuffer, lifStatus := handleLineInFile(changesets, buffer)
        if lifStatus {
            buffer.Reset()
            buffer.WriteString(lifBuffer.String())
            writeBufferToFile = lifStatus
        }
    }

    // --output
    // --output-strip-ext
    // enforcing writing of file (creating new file)
    if (opts.Output != "" || opts.OutputStripFileExt != "") {
        writeBufferToFile = true
    }

    if writeBufferToFile {
        output, status = writeContentToFile(fileitem, buffer)
    } else {
        output = fmt.Sprintf("%s no match", fileitem.Path)
    }

    return output, status, err
}

// Apply changesets to file
func applyTemplateToFile(fileitem fileitem, changesets []changeset) (string, bool, error) {
    var (
        err error = nil
        output string = ""
        status bool = true
    )

    // try open file
    buffer, err := ioutil.ReadFile(fileitem.Path)
    if err != nil {
        return output, false, err
    }

    content := parseContentAsTemplate(string(buffer), changesets)

    output, status = writeContentToFile(fileitem, content)

    return output, status, err
}

func applyChangesetsToLine(line string, changesets []changeset) (string, bool, bool) {
    changed := false
    skipLine := false

    for i, changeset := range changesets {
        // --once, only do changeset once if already applied to file
        if opts.Once != "" && changeset.MatchFound {
            // --once=unique, skip matching lines
            if opts.Once == "unique" && searchMatch(line, changeset) {
                // matching line, not writing to buffer as requsted
                skipLine = true
                changed = true
                break
            }
        } else {
            // search and replace
            if searchMatch(line, changeset) {
                // --mode=line or --mode=lineinfile
                if opts.ModeIsReplaceLine || opts.ModeIsLineInFile {
                    if opts.RegexBackref {
                        // get match
                        line = string(changeset.Search.Find([]byte(line)))

                        // replace regex backrefs in match
                        line = changeset.Search.ReplaceAllString(line, changeset.Replace)
                    } else {
                        // replace whole line with replace term
                        line = changeset.Replace
                    }
                } else {
                    // replace only term inside line
                    line = replaceText(line, changeset)
                }

                changesets[i].MatchFound = true
                changed = true
            }
        }
    }

    return line, changed, skipLine
}

// Build search term
// Compiles regexp if regexp is used
func buildSearchTerm(term string) (*regexp.Regexp) {
    var ret *regexp.Regexp
    var regex string

    // --regex
    if opts.Regex {
        // use search term as regex
        regex = term
    } else {
        // use search term as normal string, escape it for regex usage
        regex = regexp.QuoteMeta(term)
    }

    // --ignore-case
    if opts.CaseInsensitive {
        regex = "(?i:" + regex + ")"
    }

    // --verbose
    if opts.Verbose {
        logMessage(fmt.Sprintf("Using regular expression: %s", regex))
    }

    // --regex-posix
    if opts.RegexPosix {
        ret = regexp.MustCompilePOSIX(regex)
    } else {
        ret = regexp.MustCompile(regex)
    }

    return ret
}

// handle special cli options
// eg. --help
//     --version
//     --path
//     --mode=...
func handleSpecialCliOptions(args []string) {
    // --dumpversion
    if (opts.ShowOnlyVersion) {
        fmt.Println(Version)
        os.Exit(0)
    }

    // --version
    if (opts.ShowVersion) {
        fmt.Println(fmt.Sprintf("go-replace version %s", Version))
        fmt.Println(fmt.Sprintf("Copyright (C) 2017 %s", Author))
        os.Exit(0)
    }

    // --help
    if (opts.ShowHelp) {
        argparser.WriteHelp(os.Stdout)
        os.Exit(1)
    }

    // --mode
    switch mode := opts.Mode; mode {
        case "replace":
            opts.ModeIsReplaceMatch = true
            opts.ModeIsReplaceLine = false
            opts.ModeIsLineInFile = false
            opts.ModeIsTemplate = false
        case "line":
            opts.ModeIsReplaceMatch = false
            opts.ModeIsReplaceLine = true
            opts.ModeIsLineInFile = false
            opts.ModeIsTemplate = false
        case "lineinfile":
            opts.ModeIsReplaceMatch = false
            opts.ModeIsReplaceLine = false
            opts.ModeIsLineInFile = true
            opts.ModeIsTemplate = false
        case "template":
            opts.ModeIsReplaceMatch = false
            opts.ModeIsReplaceLine = false
            opts.ModeIsLineInFile = false
            opts.ModeIsTemplate = true
    }

    // --output
    if (opts.Output != "" && len(args) > 1) {
        logFatalErrorAndExit(errors.New("Only one file is allowed when using --output"), 1)
    }

    if opts.LineinfileBefore != "" || opts.LineinfileAfter != "" {
        if ! opts.ModeIsLineInFile {
            logFatalErrorAndExit(errors.New("--lineinfile-after and --lineinfile-before only valid in --mode=lineinfile"), 1)
        }

        if opts.LineinfileBefore != "" && opts.LineinfileAfter != "" {
            logFatalErrorAndExit(errors.New("Only --lineinfile-after or --lineinfile-before is allowed in --mode=lineinfile"), 1)
        }
    }
}

func getFilelistByPath() []string {
    var ret []string

    // --path
    if (opts.Path != "") {
        searchFilesInPath(opts.Path, func(f os.FileInfo, path string) {
            ret = append(ret, path)
        })
    }

    return ret
}

func actionProcessStdinReplace(changesets []changeset) (int) {
    scanner := bufio.NewScanner(os.Stdin)
    for scanner.Scan() {
        line := scanner.Text()

        newLine, _, skipLine := applyChangesetsToLine(line, changesets)

        if !skipLine {
            fmt.Println(newLine)
        }
    }

    return 0
}

func actionProcessStdinTemplate(changesets []changeset) (int) {
    var buffer bytes.Buffer

    scanner := bufio.NewScanner(os.Stdin)
    for scanner.Scan() {
        buffer.WriteString(scanner.Text() + "\n")
    }

    content := parseContentAsTemplate(buffer.String(), changesets)
    fmt.Print(content.String())

    return 0
}

func actionProcessFiles(changesets []changeset, fileitems []fileitem) (int) {
    // check if there is at least one file to process
    if (len(fileitems) == 0) {
        if (opts.IgnoreEmpty) {
            // no files found, but we should ignore empty filelist
            logMessage("No files found, requsted to ignore this")
            os.Exit(0)
        } else {
            // no files found, print error and exit with error code
            logFatalErrorAndExit(errors.New("No files specified"), 1)
        }
    }

    swg := sizedwaitgroup.New(8)
    results := make(chan changeresult, len(fileitems))

    // process file list
    for _, file := range fileitems {
        fmt.Println(file.Path)
        swg.Add()
        go func(file fileitem, changesets []changeset) {
            var (
                err error = nil
                output string = ""
                status bool = true
            )

            if opts.ModeIsTemplate {
                output, status, err = applyTemplateToFile(file, changesets)
            } else {
                output, status, err = applyChangesetsToFile(file, changesets)
            }

            results <- changeresult{file, output, status, err}
            swg.Done()
        } (file, changesets);
    }

    // wait for all changes to be processed
    swg.Wait()
    close(results)

    // show results
    errorCount := 0
    for result := range results {
        if result.Error != nil {
            logError(result.Error)
            errorCount++
        } else if opts.Verbose {
            title := fmt.Sprintf("%s:", result.File.Path)

            fmt.Fprintln(os.Stderr, "")
            fmt.Fprintln(os.Stderr, title)
            fmt.Fprintln(os.Stderr, strings.Repeat("-", len(title)))
            fmt.Fprintln(os.Stderr, "")
            fmt.Fprintln(os.Stderr, result.Output)
            fmt.Fprintln(os.Stderr, "")
        }
    }

    if errorCount >= 1 {
        fmt.Fprintln(os.Stderr, fmt.Sprintf("[ERROR] %s failed with %d error(s)", argparser.Command.Name, errorCount))
        return 1
    }

    return 0
}

func buildChangesets() ([]changeset){
    var changesets []changeset

    if !opts.ModeIsTemplate {
        if len(opts.Search) == 0 || len(opts.Replace) == 0 {
            // error: unequal numbers of search and replace options
            logFatalErrorAndExit(errors.New("Missing either --search or --replace for this mode"), 1)
        }
    }

    // check if search and replace options have equal lenght (equal number of options)
    if len(opts.Search) != len(opts.Replace) {
        // error: unequal numbers of search and replace options
        logFatalErrorAndExit(errors.New("Unequal numbers of search or replace options"), 1)
    }

    // build changesets
    for i := range opts.Search {
        search := opts.Search[i]
        replace := opts.Replace[i]

        changeset := changeset{search, buildSearchTerm(search), replace, false}
        changesets = append(changesets, changeset)
    }

    return changesets
}

func buildFileitems(args []string) ([]fileitem) {
    var (
        fileitems []fileitem
        file fileitem
    )

    for _, filepath := range args {
        file = fileitem{filepath, filepath}

        if opts.Output != "" {
            // use specific output
            file.Output = opts.Output
        } else if opts.OutputStripFileExt != "" {
            // remove file ext from saving destination
            file.Output = strings.TrimSuffix(file.Output, opts.OutputStripFileExt)
        } else if strings.Contains(filepath, ":") {
            // argument like "source:destination"
            split := strings.SplitN(filepath, ":", 2)

            file.Path = split[0]
            file.Output = split[1]
        }

        fileitems = append(fileitems, file)
    }

    // --path parsing
    if opts.Path != "" {
        for _, filepath := range getFilelistByPath() {
            file := fileitem{filepath, filepath}

            if opts.Output != "" {
                // use specific output
                file.Output = opts.Output
            } else if opts.OutputStripFileExt != "" {
                // remove file ext from saving destination
                file.Output = strings.TrimSuffix(file.Output, opts.OutputStripFileExt)
            }

            // no colon parsing here

            fileitems = append(fileitems, file)
        }
    }

    return fileitems
}

var argparser *flags.Parser
func main() {
    argparser = flags.NewParser(&opts, flags.PassDoubleDash)
    args, err := argparser.Parse()

    handleSpecialCliOptions(args)

    // check if there is an parse error
    if err != nil {
        logFatalErrorAndExit(err, 1)
    }

    changesets := buildChangesets()
    fileitems := buildFileitems(args)

    exitMode := 0
    if opts.Stdin {
        if opts.ModeIsTemplate {
            // use stdin as input
            exitMode = actionProcessStdinTemplate(changesets)
        } else {
            // use stdin as input
            exitMode = actionProcessStdinReplace(changesets)
        }
    } else {
        // use and process files (see args)
        exitMode = actionProcessFiles(changesets, fileitems)
    }

    os.Exit(exitMode)
}
<commit_msg>Cleanup<commit_after>package main

import (
    "fmt"
    "errors"
    "bytes"
    "io/ioutil"
    "bufio"
    "os"
    "strings"
    "regexp"
    flags "github.com/jessevdk/go-flags"
    "github.com/remeh/sizedwaitgroup"
)

const (
    Author  = "webdevops.io"
    Version = "1.1.1"
)

type changeset struct {
    SearchPlain string
    Search      *regexp.Regexp
    Replace     string
    MatchFound  bool
}

type changeresult struct {
    File   fileitem
    Output string
    Status bool
    Error  error
}

type fileitem struct {
    Path        string
    Output      string
}

var opts struct {
    ThreadCount             int      `           long:"threads"                       description:"Set thread concurrency for replacing in multiple files at same time" default:"20"`
    Mode                    string   `short:"m"  long:"mode"                          description:"replacement mode - replace: replace match with term; line: replace line with term; lineinfile: replace line with term or if not found append to term to file; template: parse content as golang template, search value have to start uppercase" default:"replace" choice:"replace" choice:"line" choice:"lineinfile" choice:"template"`
    ModeIsReplaceMatch      bool
    ModeIsReplaceLine       bool
    ModeIsLineInFile        bool
    ModeIsTemplate          bool
    Search                  []string `short:"s"  long:"search"                        description:"search term"`
    Replace                 []string `short:"r"  long:"replace"                       description:"replacement term"`
    LineinfileBefore        string   `           long:"lineinfile-before"             description:"add line before this regex"`
    LineinfileAfter         string   `           long:"lineinfile-after"              description:"add line after this regex"`
    CaseInsensitive         bool     `short:"i"  long:"case-insensitive"              description:"ignore case of pattern to match upper and lowercase characters"`
    Stdin                   bool     `           long:"stdin"                         description:"process stdin as input"`
    Output                  string   `short:"o"  long:"output"                        description:"write changes to this file (in one file mode)"`
    OutputStripFileExt      string   `           long:"output-strip-ext"              description:"strip file extension from written files (also available in multi file mode)"`
    Once                    string   `           long:"once"                          description:"replace search term only one in a file, keep duplicaes (keep, default) or remove them (unique)" optional:"true" optional-value:"keep" choice:"keep" choice:"unique"`
    Regex                   bool     `           long:"regex"                         description:"treat pattern as regex"`
    RegexBackref            bool     `           long:"regex-backrefs"                description:"enable backreferences in replace term"`
    RegexPosix              bool     `           long:"regex-posix"                   description:"parse regex term as POSIX regex"`
    Path                    string   `           long:"path"                          description:"use files in this path"`
    PathPattern             string   `           long:"path-pattern"                  description:"file pattern (* for wildcard, only basename of file)"`
    PathRegex               string   `           long:"path-regex"                    description:"file pattern (regex, full path)"`
    IgnoreEmpty             bool     `           long:"ignore-empty"                  description:"ignore empty file list, otherwise this will result in an error"`
    Verbose                 bool     `short:"v"  long:"verbose"                       description:"verbose mode"`
    DryRun                  bool     `           long:"dry-run"                       description:"dry run mode"`
    ShowVersion             bool     `short:"V"  long:"version"                       description:"show version and exit"`
    ShowOnlyVersion         bool     `           long:"dumpversion"                   description:"show only version number and exit"`
    ShowHelp                bool     `short:"h"  long:"help"                          description:"show this help message"`
}

var pathFilterDirectories = []string{"autom4te.cache", "blib", "_build", ".bzr", ".cdv", "cover_db", "CVS", "_darcs", "~.dep", "~.dot", ".git", ".hg", "~.nib", ".pc", "~.plst", "RCS", "SCCS", "_sgbak", ".svn", "_obj", ".idea"}

// Apply changesets to file
func applyChangesetsToFile(fileitem fileitem, changesets []changeset) (string, bool, error) {
    var (
        err error = nil
        output string = ""
        status bool = true
    )

    // try open file
    file, err := os.Open(fileitem.Path)
    if err != nil {
        return output, false, err
    }

    writeBufferToFile := false
    var buffer bytes.Buffer

    r := bufio.NewReader(file)
    line, e := Readln(r)
    for e == nil {
        newLine, lineChanged, skipLine := applyChangesetsToLine(line, changesets)

        if lineChanged || skipLine {
            writeBufferToFile = true
        }

        if !skipLine {
            buffer.WriteString(newLine + "\n")
        }

        line, e = Readln(r)
    }
    file.Close()

    // --mode=lineinfile
    if opts.ModeIsLineInFile {
        lifBuffer, lifStatus := handleLineInFile(changesets, buffer)
        if lifStatus {
            buffer.Reset()
            buffer.WriteString(lifBuffer.String())
            writeBufferToFile = lifStatus
        }
    }

    // --output
    // --output-strip-ext
    // enforcing writing of file (creating new file)
    if (opts.Output != "" || opts.OutputStripFileExt != "") {
        writeBufferToFile = true
    }

    if writeBufferToFile {
        output, status = writeContentToFile(fileitem, buffer)
    } else {
        output = fmt.Sprintf("%s no match", fileitem.Path)
    }

    return output, status, err
}

// Apply changesets to file
func applyTemplateToFile(fileitem fileitem, changesets []changeset) (string, bool, error) {
    var (
        err error = nil
        output string = ""
        status bool = true
    )

    // try open file
    buffer, err := ioutil.ReadFile(fileitem.Path)
    if err != nil {
        return output, false, err
    }

    content := parseContentAsTemplate(string(buffer), changesets)

    output, status = writeContentToFile(fileitem, content)

    return output, status, err
}

func applyChangesetsToLine(line string, changesets []changeset) (string, bool, bool) {
    changed := false
    skipLine := false

    for i, changeset := range changesets {
        // --once, only do changeset once if already applied to file
        if opts.Once != "" && changeset.MatchFound {
            // --once=unique, skip matching lines
            if opts.Once == "unique" && searchMatch(line, changeset) {
                // matching line, not writing to buffer as requsted
                skipLine = true
                changed = true
                break
            }
        } else {
            // search and replace
            if searchMatch(line, changeset) {
                // --mode=line or --mode=lineinfile
                if opts.ModeIsReplaceLine || opts.ModeIsLineInFile {
                    if opts.RegexBackref {
                        // get match
                        line = string(changeset.Search.Find([]byte(line)))

                        // replace regex backrefs in match
                        line = changeset.Search.ReplaceAllString(line, changeset.Replace)
                    } else {
                        // replace whole line with replace term
                        line = changeset.Replace
                    }
                } else {
                    // replace only term inside line
                    line = replaceText(line, changeset)
                }

                changesets[i].MatchFound = true
                changed = true
            }
        }
    }

    return line, changed, skipLine
}

// Build search term
// Compiles regexp if regexp is used
func buildSearchTerm(term string) (*regexp.Regexp) {
    var ret *regexp.Regexp
    var regex string

    // --regex
    if opts.Regex {
        // use search term as regex
        regex = term
    } else {
        // use search term as normal string, escape it for regex usage
        regex = regexp.QuoteMeta(term)
    }

    // --ignore-case
    if opts.CaseInsensitive {
        regex = "(?i:" + regex + ")"
    }

    // --verbose
    if opts.Verbose {
        logMessage(fmt.Sprintf("Using regular expression: %s", regex))
    }

    // --regex-posix
    if opts.RegexPosix {
        ret = regexp.MustCompilePOSIX(regex)
    } else {
        ret = regexp.MustCompile(regex)
    }

    return ret
}

// handle special cli options
// eg. --help
//     --version
//     --path
//     --mode=...
func handleSpecialCliOptions(args []string) {
    // --dumpversion
    if (opts.ShowOnlyVersion) {
        fmt.Println(Version)
        os.Exit(0)
    }

    // --version
    if (opts.ShowVersion) {
        fmt.Println(fmt.Sprintf("go-replace version %s", Version))
        fmt.Println(fmt.Sprintf("Copyright (C) 2017 %s", Author))
        os.Exit(0)
    }

    // --help
    if (opts.ShowHelp) {
        argparser.WriteHelp(os.Stdout)
        os.Exit(1)
    }

    // --mode
    switch mode := opts.Mode; mode {
        case "replace":
            opts.ModeIsReplaceMatch = true
            opts.ModeIsReplaceLine = false
            opts.ModeIsLineInFile = false
            opts.ModeIsTemplate = false
        case "line":
            opts.ModeIsReplaceMatch = false
            opts.ModeIsReplaceLine = true
            opts.ModeIsLineInFile = false
            opts.ModeIsTemplate = false
        case "lineinfile":
            opts.ModeIsReplaceMatch = false
            opts.ModeIsReplaceLine = false
            opts.ModeIsLineInFile = true
            opts.ModeIsTemplate = false
        case "template":
            opts.ModeIsReplaceMatch = false
            opts.ModeIsReplaceLine = false
            opts.ModeIsLineInFile = false
            opts.ModeIsTemplate = true
    }

    // --output
    if (opts.Output != "" && len(args) > 1) {
        logFatalErrorAndExit(errors.New("Only one file is allowed when using --output"), 1)
    }

    if opts.LineinfileBefore != "" || opts.LineinfileAfter != "" {
        if ! opts.ModeIsLineInFile {
            logFatalErrorAndExit(errors.New("--lineinfile-after and --lineinfile-before only valid in --mode=lineinfile"), 1)
        }

        if opts.LineinfileBefore != "" && opts.LineinfileAfter != "" {
            logFatalErrorAndExit(errors.New("Only --lineinfile-after or --lineinfile-before is allowed in --mode=lineinfile"), 1)
        }
    }
}

func actionProcessStdinReplace(changesets []changeset) (int) {
    scanner := bufio.NewScanner(os.Stdin)
    for scanner.Scan() {
        line := scanner.Text()

        newLine, _, skipLine := applyChangesetsToLine(line, changesets)

        if !skipLine {
            fmt.Println(newLine)
        }
    }

    return 0
}

func actionProcessStdinTemplate(changesets []changeset) (int) {
    var buffer bytes.Buffer

    scanner := bufio.NewScanner(os.Stdin)
    for scanner.Scan() {
        buffer.WriteString(scanner.Text() + "\n")
    }

    content := parseContentAsTemplate(buffer.String(), changesets)
    fmt.Print(content.String())

    return 0
}

func actionProcessFiles(changesets []changeset, fileitems []fileitem) (int) {
    // check if there is at least one file to process
    if (len(fileitems) == 0) {
        if (opts.IgnoreEmpty) {
            // no files found, but we should ignore empty filelist
            logMessage("No files found, requsted to ignore this")
            os.Exit(0)
        } else {
            // no files found, print error and exit with error code
            logFatalErrorAndExit(errors.New("No files specified"), 1)
        }
    }

    swg := sizedwaitgroup.New(8)
    results := make(chan changeresult, len(fileitems))

    // process file list
    for _, file := range fileitems {
        swg.Add()
        go func(file fileitem, changesets []changeset) {
            var (
                err error = nil
                output string = ""
                status bool = true
            )

            if opts.ModeIsTemplate {
                output, status, err = applyTemplateToFile(file, changesets)
            } else {
                output, status, err = applyChangesetsToFile(file, changesets)
            }

            results <- changeresult{file, output, status, err}
            swg.Done()
        } (file, changesets);
    }

    // wait for all changes to be processed
    swg.Wait()
    close(results)

    // show results
    errorCount := 0
    for result := range results {
        if result.Error != nil {
            logError(result.Error)
            errorCount++
        } else if opts.Verbose {
            title := fmt.Sprintf("%s:", result.File.Path)

            fmt.Fprintln(os.Stderr, "")
            fmt.Fprintln(os.Stderr, title)
            fmt.Fprintln(os.Stderr, strings.Repeat("-", len(title)))
            fmt.Fprintln(os.Stderr, "")
            fmt.Fprintln(os.Stderr, result.Output)
            fmt.Fprintln(os.Stderr, "")
        }
    }

    if errorCount >= 1 {
        fmt.Fprintln(os.Stderr, fmt.Sprintf("[ERROR] %s failed with %d error(s)", argparser.Command.Name, errorCount))
        return 1
    }

    return 0
}

func buildChangesets() ([]changeset){
    var changesets []changeset

    if !opts.ModeIsTemplate {
        if len(opts.Search) == 0 || len(opts.Replace) == 0 {
            // error: unequal numbers of search and replace options
            logFatalErrorAndExit(errors.New("Missing either --search or --replace for this mode"), 1)
        }
    }

    // check if search and replace options have equal lenght (equal number of options)
    if len(opts.Search) != len(opts.Replace) {
        // error: unequal numbers of search and replace options
        logFatalErrorAndExit(errors.New("Unequal numbers of search or replace options"), 1)
    }

    // build changesets
    for i := range opts.Search {
        search := opts.Search[i]
        replace := opts.Replace[i]

        changeset := changeset{search, buildSearchTerm(search), replace, false}
        changesets = append(changesets, changeset)
    }

    return changesets
}

func buildFileitems(args []string) ([]fileitem) {
    var (
        fileitems []fileitem
        file fileitem
    )

    // Build filelist from arguments
    for _, filepath := range args {
        file = fileitem{filepath, filepath}

        if opts.Output != "" {
            // use specific output
            file.Output = opts.Output
        } else if opts.OutputStripFileExt != "" {
            // remove file ext from saving destination
            file.Output = strings.TrimSuffix(file.Output, opts.OutputStripFileExt)
        } else if strings.Contains(filepath, ":") {
            // argument like "source:destination"
            split := strings.SplitN(filepath, ":", 2)

            file.Path = split[0]
            file.Output = split[1]
        }

        fileitems = append(fileitems, file)
    }

    // --path parsing
    if opts.Path != "" {
        searchFilesInPath(opts.Path, func(f os.FileInfo, filepath string) {
            file := fileitem{filepath, filepath}

            if opts.OutputStripFileExt != "" {
                // remove file ext from saving destination
                file.Output = strings.TrimSuffix(file.Output, opts.OutputStripFileExt)
            }

            // no colon parsing here

            fileitems = append(fileitems, file)
        })
    }

    return fileitems
}

var argparser *flags.Parser
func main() {
    argparser = flags.NewParser(&opts, flags.PassDoubleDash)
    args, err := argparser.Parse()

    handleSpecialCliOptions(args)

    // check if there is an parse error
    if err != nil {
        logFatalErrorAndExit(err, 1)
    }

    changesets := buildChangesets()
    fileitems := buildFileitems(args)

    exitMode := 0
    if opts.Stdin {
        if opts.ModeIsTemplate {
            // use stdin as input
            exitMode = actionProcessStdinTemplate(changesets)
        } else {
            // use stdin as input
            exitMode = actionProcessStdinReplace(changesets)
        }
    } else {
        // use and process files (see args)
        exitMode = actionProcessFiles(changesets, fileitems)
    }

    os.Exit(exitMode)
}
<|endoftext|>


<commit_before>package main

import (
	"bytes"
	"flag"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"text/template"

	utils "github.com/shurcooL/github_flavored_markdown"
)

var (
	file string
	bind string
)

var html = `

    <meta charset="utf-8">
    <link rel="stylesheet" href="./mdp.css" type="text/css" media="screen" charset="utf-8">
    <link href="https://assets-cdn.github.com/assets/frameworks-343a7fdeaa4388a32c78fff00bca4c2f2b7d112375af9b44bdbaed82c48ad4ee.css" media="all" rel="stylesheet" type="text/css" />
    <link href="https://assets-cdn.github.com/assets/github-82746a5e80e1762d01af3e079408b886361d5fe5339de04edb1cd6df16c24eb2.css" media="all" rel="stylesheet" type="text/css" />
    <link href="//cdnjs.cloudflare.com/ajax/libs/octicons/2.1.2/octicons.css" media="all" rel="stylesheet" type="text/css" />

      {{.}}

`

func init() {
	flag.StringVar(&bind, "bind", ":8080", "interface to bind to, eg. 0.0.0.0:8080")
	flag.StringVar(&file, "file", "README.md", "file to render on web interface")
}

func main() {
	flag.Parse()
	http.HandleFunc("/", Handler)
	log.Printf("Serving file %s on interface %s\n", file, bind)
	log.Fatal(http.ListenAndServe(bind, nil))
}

func Handler(res http.ResponseWriter, req *http.Request) {
	readme, err := GetReadme()
	if err != nil {
		fmt.Fprintf(res, "Something went wrong:\n%s", err)
		return
	}

	fmt.Fprintf(res, string(readme))
}

func GetReadme() ([]byte, error) {
	tpl, err := template.New("html").Parse(html)
	if err != nil {
		return nil, err
	}

	b, err := ioutil.ReadFile(file)
	if err != nil {
		return nil, err
	}
	md := utils.Markdown(b)

	x := make([]byte, 0)
	buf := bytes.NewBuffer(x)
	err = tpl.Execute(buf, string(md))
	if err != nil {
		return nil, err
	}

	return buf.Bytes(), nil
}
<commit_msg>Add version.<commit_after>package main

import (
	"bytes"
	"flag"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"text/template"

	utils "github.com/shurcooL/github_flavored_markdown"
)

const (
	VERSION = "0.1.0"
)

var (
	file    string
	bind    string
	version bool
)

var html = `

    <meta charset="utf-8">
    <link rel="stylesheet" href="./mdp.css" type="text/css" media="screen" charset="utf-8">
    <link href="https://assets-cdn.github.com/assets/frameworks-343a7fdeaa4388a32c78fff00bca4c2f2b7d112375af9b44bdbaed82c48ad4ee.css" media="all" rel="stylesheet" type="text/css" />
    <link href="https://assets-cdn.github.com/assets/github-82746a5e80e1762d01af3e079408b886361d5fe5339de04edb1cd6df16c24eb2.css" media="all" rel="stylesheet" type="text/css" />
    <link href="//cdnjs.cloudflare.com/ajax/libs/octicons/2.1.2/octicons.css" media="all" rel="stylesheet" type="text/css" />

      {{.}}

`

func init() {
	flag.StringVar(&bind, "bind", ":8080", "interface to bind to, eg. 0.0.0.0:8080")
	flag.StringVar(&file, "file", "README.md", "file to render on web interface")
	flag.BoolVar(&version, "version", false, "prints out the version")
}

func main() {
	flag.Parse()
	if version {
		fmt.Println(VERSION)
		os.Exit(0)
	}

	http.HandleFunc("/", Handler)
	log.Printf("Serving file %s on interface %s\n", file, bind)
	log.Fatal(http.ListenAndServe(bind, nil))
}

func Handler(res http.ResponseWriter, req *http.Request) {
	readme, err := GetReadme()
	if err != nil {
		fmt.Fprintf(res, "Something went wrong:\n%s", err)
		return
	}

	fmt.Fprintf(res, string(readme))
}

func GetReadme() ([]byte, error) {
	tpl, err := template.New("html").Parse(html)
	if err != nil {
		return nil, err
	}

	b, err := ioutil.ReadFile(file)
	if err != nil {
		return nil, err
	}
	md := utils.Markdown(b)

	x := make([]byte, 0)
	buf := bytes.NewBuffer(x)
	err = tpl.Execute(buf, string(md))
	if err != nil {
		return nil, err
	}

	return buf.Bytes(), nil
}
<|endoftext|>


<commit_before>package main

import (
	"flag"
	"fmt"
	"io"
	"net/http"
	"os"

	"github.com/rs/cors"
	"github.com/urfave/negroni"

	"github.com/yuuki/diamondb/lib/config"
	"github.com/yuuki/diamondb/lib/log"
	"github.com/yuuki/diamondb/lib/web"
)

// CLI is the command line object
type CLI struct {
	// outStream and errStream are the stdout and stderr
	// to write message from the CLI.
	outStream, errStream io.Writer
}

func main() {
	cli := &CLI{outStream: os.Stdout, errStream: os.Stderr}
	os.Exit(cli.Run(os.Args))
}

// Run invokes the CLI with the given arguments.
func (cli *CLI) Run(args []string) int {
	var (
		host    string
		port    string
		version bool
		debug   bool
	)

	flags := flag.NewFlagSet(Name, flag.ContinueOnError)
	flags.SetOutput(cli.errStream)
	flags.Usage = func() {
		fmt.Fprint(cli.errStream, helpText)
	}
	flags.StringVar(&host, "host", config.DefaultHost, "")
	flags.StringVar(&host, "H", config.DefaultHost, "")
	flags.StringVar(&port, "port", config.DefaultPort, "")
	flags.StringVar(&port, "P", config.DefaultPort, "")
	flags.BoolVar(&version, "version", false, "")
	flags.BoolVar(&version, "v", false, "")
	flags.BoolVar(&debug, "debug", false, "")
	flags.BoolVar(&debug, "d", false, "")

	if err := flags.Parse(args[1:]); err != nil {
		return 1
	}
	log.SetDebug(debug)

	if version {
		fmt.Fprintf(cli.errStream, "%s version %s, build %s \n", Name, Version, GitCommit)
		return 0
	}

	if err := config.Load(); err != nil {
		log.Printf("Failed to load the config: %s", err)
		return 2
	}

	mux := http.NewServeMux()
	mux.HandleFunc("/render", web.Render)

	n := negroni.New()
	n.Use(negroni.NewRecovery())
	n.Use(negroni.NewLogger())
	n.Use(cors.New(cors.Options{
		AllowedOrigins: []string{"*"},
		AllowedMethods: []string{"GET", "POST"},
		AllowedHeaders: []string{"Origin", "Accept", "Content-Type"},
	}))
	n.UseHandler(mux)

	log.Printf("Listening %s:%s ...", host, port)
	if err := http.ListenAndServe(":"+port, n); err != nil {
		log.Println(err)
		return 3
	}

	return 0
}

var helpText = `
Usage: diamondb [options]

  diamondb is the DynamoDB-based TSDB API server.

Options:
  --host, -H           Bind host

  --port, -P           Listen port

  --debug, -d          Run with debug print
`
<commit_msg>Make flag prior<commit_after>package main

import (
	"flag"
	"fmt"
	"io"
	"net/http"
	"os"

	"github.com/rs/cors"
	"github.com/urfave/negroni"

	"github.com/yuuki/diamondb/lib/config"
	"github.com/yuuki/diamondb/lib/log"
	"github.com/yuuki/diamondb/lib/web"
)

// CLI is the command line object
type CLI struct {
	// outStream and errStream are the stdout and stderr
	// to write message from the CLI.
	outStream, errStream io.Writer
}

func main() {
	cli := &CLI{outStream: os.Stdout, errStream: os.Stderr}
	os.Exit(cli.Run(os.Args))
}

// Run invokes the CLI with the given arguments.
func (cli *CLI) Run(args []string) int {
	if err := config.Load(); err != nil {
		log.Printf("Failed to load the config: %s", err)
		return 2
	}

	var (
		host    string
		port    string
		version bool
		debug   bool
	)

	flags := flag.NewFlagSet(Name, flag.ContinueOnError)
	flags.SetOutput(cli.errStream)
	flags.Usage = func() {
		fmt.Fprint(cli.errStream, helpText)
	}
	flags.StringVar(&host, "host", config.Config.Host, "")
	flags.StringVar(&host, "H", config.Config.Host, "")
	flags.StringVar(&port, "port", config.Config.Port, "")
	flags.StringVar(&port, "P", config.Config.Port, "")
	flags.BoolVar(&version, "version", false, "")
	flags.BoolVar(&version, "v", false, "")
	flags.BoolVar(&debug, "debug", false, "")
	flags.BoolVar(&debug, "d", false, "")

	if err := flags.Parse(args[1:]); err != nil {
		return 1
	}
	log.SetDebug(debug)

	if version {
		fmt.Fprintf(cli.errStream, "%s version %s, build %s \n", Name, Version, GitCommit)
		return 0
	}

	mux := http.NewServeMux()
	mux.HandleFunc("/render", web.Render)

	n := negroni.New()
	n.Use(negroni.NewRecovery())
	n.Use(negroni.NewLogger())
	n.Use(cors.New(cors.Options{
		AllowedOrigins: []string{"*"},
		AllowedMethods: []string{"GET", "POST"},
		AllowedHeaders: []string{"Origin", "Accept", "Content-Type"},
	}))
	n.UseHandler(mux)

	log.Printf("Listening %s:%s ...", host, port)
	if err := http.ListenAndServe(":"+port, n); err != nil {
		log.Println(err)
		return 3
	}

	return 0
}

var helpText = `
Usage: diamondb [options]

  diamondb is the DynamoDB-based TSDB API server.

Options:
  --host, -H           Bind host

  --port, -P           Listen port

  --debug, -d          Run with debug print
`
<|endoftext|>


<commit_before>package main

import (
	"flag"
	"fmt"
)

func main() {

	versionFlag := flag.Bool("version", false, "Version")
	flag.Parse()

	if *versionFlag {
		fmt.Println("Git Commit:", GitCommit)
		fmt.Println("Version:", Version)
		if VersionPrerelease != "" {
			fmt.Println("Version PreRelease:", VersionPrerelease)
		}
		return
	}

	fmt.Println("Hello.")
}
<commit_msg>Add initial server<commit_after>package main

import (
	"flag"
	"fmt"
	"net"

	"github.com/dustin/go-coap"
)

// check is a simple wrapper around the verbose
// `if err != nil` checks.
func check(err error) {
	if err != nil {
		panic(err)
	}
}

// Metric is the layout of the metric payload from a client.
type Metric struct {
	Fqdn    uint16
	Service uint8
	Key     uint16
	Value   string
}

func main() {

	versionFlag := flag.Bool("version", false, "Version")
	flag.Parse()

	if *versionFlag {
		fmt.Println("Git Commit:", GitCommit)
		fmt.Println("Version:", Version)
		if VersionPrerelease != "" {
			fmt.Println("Version PreRelease:", VersionPrerelease)
		}
		return
	}

	coap.ListenAndServe("udp", ":5683",
		coap.FuncHandler(func(l *net.UDPConn, a *net.UDPAddr, m *coap.Message) *coap.Message {
			fmt.Printf("Go message path=%q: %#v from %v\n", m.Path(), m, a)
			fmt.Printf("Payload: %#v\n", m.Payload)
			if m.IsConfirmable() {
				res := &coap.Message{
					Type:      coap.Acknowledgement,
					Code:      coap.Content,
					MessageID: m.MessageID,
					Token:     m.Token,
					Payload:   []byte("Halp"),
				}
				res.SetOption(coap.ContentFormat, coap.TextPlain)

				return res
			}
			return nil
		}),
	)
}
<|endoftext|>


<commit_before>package main

import (
	"context"
	"encoding/json"
	"log"
	"net/http"
	"os"
	"os/signal"
	"path/filepath"
	"strings"
	"syscall"
	"time"

	"github.com/meiraka/vv/internal/mpd"
	"github.com/meiraka/vv/internal/songs/cover"
)

const (
	defaultConfigDir = "/etc/xdg/vv"
)

var version = "v0.10.2+"

//go:generate go run internal/cmd/fix-assets/main.go
func main() {
	v2()
}

func configDirs() []string {
	dir, err := os.UserConfigDir()
	if err != nil {
		return []string{defaultConfigDir}
	}
	return []string{filepath.Join(dir, "vv"), defaultConfigDir}
}

func v2() {
	ctx := context.TODO()
	config, date, err := ParseConfig(configDirs(), "config.yaml")
	if err != nil {
		log.Fatalf("failed to load config: %v", err)
	}
	dialer := mpd.Dialer{
		Timeout:              10 * time.Second,
		HealthCheckInterval:  time.Second,
		ReconnectionInterval: 5 * time.Second,
	}
	tree, err := json.Marshal(config.Playlist.Tree)
	if err != nil {
		log.Fatalf("failed to create playlist tree: %v", err)
	}
	treeOrder, err := json.Marshal(config.Playlist.TreeOrder)
	if err != nil {
		log.Fatalf("failed to create playlist tree order: %v", err)
	}
	cl, err := dialer.Dial(config.MPD.Network, config.MPD.Addr, "")
	if err != nil {
		log.Fatalf("failed to dial mpd: %v", err)
	}
	w, err := dialer.NewWatcher(config.MPD.Network, config.MPD.Addr, "")
	if err != nil {
		log.Fatalf("failed to dial mpd: %v", err)
	}
	commands, err := cl.Commands(ctx)
	if err != nil {
		log.Fatalf("failed to check mpd supported functions: %v", err)
	}
	// get music dir from local mpd connection
	if config.MPD.Network == "unix" && config.MPD.MusicDirectory == "" {
		if c, err := cl.Config(ctx); err == nil {
			if dir, ok := c["music_directory"]; ok && filepath.IsAbs(dir) {
				config.MPD.MusicDirectory = dir
				log.Printf("apply mpd.music_directory from mpd connection: %s", dir)
			}
		}
	}

	// get music dir from local mpd config
	mpdConf, _ := mpd.ParseConfig(config.MPD.Conf)
	if config.MPD.MusicDirectory == "" {
		if mpdConf != nil && filepath.IsAbs(config.MPD.Conf) {
			config.MPD.MusicDirectory = mpdConf.MusicDirectory
			log.Printf("apply mpd.music_directory from %s: %s", config.MPD.Conf, mpdConf.MusicDirectory)
		}
	}
	proxy := map[string]string{}
	if mpdConf != nil {
		host := "localhost"
		if config.MPD.Network == "tcp" {
			h := strings.Split(config.MPD.Addr, ":")[0]
			if len(h) != 0 {
				host = h
			}
		}
		for _, dev := range mpdConf.AudioOutputs {
			if len(dev.Port) != 0 {
				proxy[dev.Name] = "http://" + host + ":" + dev.Port
			}
		}
	}
	m := http.NewServeMux()
	covers := make([]cover.Cover, 0, 2)
	if config.Server.Cover.Local {
		if len(config.MPD.MusicDirectory) == 0 {
			log.Println("config.server.cover.local is disabled: mpd.music_directory is empty")
		} else {
			c, err := cover.NewLocal("/api/music/images/local/", config.MPD.MusicDirectory, []string{"cover.jpg", "cover.jpeg", "cover.png", "cover.gif", "cover.bmp"})
			if err != nil {
				log.Fatalf("failed to initialize coverart: %v", err)
			}
			m.Handle("/api/music/images/local/", c)
			covers = append(covers, c)

		}
	}
	if config.Server.Cover.Remote {
		if !contains(commands, "albumart") {
			log.Println("config.server.cover.remote is disabled: mpd does not support albumart command")
		} else {
			c, err := cover.NewRemote("/api/music/images/remote/", cl, filepath.Join(config.Server.CacheDirectory, "imgcache"))
			if err != nil {
				log.Fatalf("failed to initialize coverart: %v", err)
			}
			m.Handle("/api/music/images/remote/", c)
			covers = append(covers, c)
			defer c.Close()
		}
	}
	batch := cover.NewBatch(covers)
	assets := AssetsConfig{
		LocalAssets: config.debug,
		Extra: map[string]string{
			"AssetsAppCSSHash": string(AssetsAppCSSHash),
			"AssetsAppJSHash":  string(AssetsAppJSHash),
			"TREE":             string(tree),
			"TREE_ORDER":       string(treeOrder),
		},
		ExtraDate: date,
	}.NewAssetsHandler()
	api, stopAPI, err := APIConfig{
		AudioProxy: proxy,
	}.NewAPIHandler(ctx, cl, w, batch)
	if err != nil {
		log.Fatalf("failed to initialize api handler: %v", err)
	}
	m.Handle("/", assets)
	m.Handle("/api/", api)

	if err != nil {
		log.Fatalf("failed to initialize app: %v", err)
	}
	s := http.Server{
		Handler: m,
		Addr:    config.Server.Addr,
	}
	s.RegisterOnShutdown(stopAPI)
	errs := make(chan error, 1)
	go func() {
		errs <- s.ListenAndServe()
	}()
	sc := make(chan os.Signal, 1)
	signal.Notify(sc, syscall.SIGTERM, syscall.SIGINT)
	select {
	case <-sc:
	case err := <-errs:
		if err != http.ErrServerClosed {
			log.Fatalf("server stopped with error: %v", err)
		}
	}

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if err := s.Shutdown(ctx); err != nil {
		log.Printf("failed to stop http server: %v", err)
	}
	if err := cl.Close(ctx); err != nil {
		log.Printf("failed to close mpd connection(main): %v", err)
	}
	if err := w.Close(ctx); err != nil {
		log.Printf("failed to close mpd connection(event): %v", err)
	}
	if err := batch.Shutdown(ctx); err != nil {
		log.Printf("failed to stop image api: %v", err)
	}
}

func contains(list []string, item string) bool {
	for _, n := range list {
		if item == n {
			return true
		}
	}
	return false
}
<commit_msg>v0.10.3+<commit_after>package main

import (
	"context"
	"encoding/json"
	"log"
	"net/http"
	"os"
	"os/signal"
	"path/filepath"
	"strings"
	"syscall"
	"time"

	"github.com/meiraka/vv/internal/mpd"
	"github.com/meiraka/vv/internal/songs/cover"
)

const (
	defaultConfigDir = "/etc/xdg/vv"
)

var version = "v0.10.3+"

//go:generate go run internal/cmd/fix-assets/main.go
func main() {
	v2()
}

func configDirs() []string {
	dir, err := os.UserConfigDir()
	if err != nil {
		return []string{defaultConfigDir}
	}
	return []string{filepath.Join(dir, "vv"), defaultConfigDir}
}

func v2() {
	ctx := context.TODO()
	config, date, err := ParseConfig(configDirs(), "config.yaml")
	if err != nil {
		log.Fatalf("failed to load config: %v", err)
	}
	dialer := mpd.Dialer{
		Timeout:              10 * time.Second,
		HealthCheckInterval:  time.Second,
		ReconnectionInterval: 5 * time.Second,
	}
	tree, err := json.Marshal(config.Playlist.Tree)
	if err != nil {
		log.Fatalf("failed to create playlist tree: %v", err)
	}
	treeOrder, err := json.Marshal(config.Playlist.TreeOrder)
	if err != nil {
		log.Fatalf("failed to create playlist tree order: %v", err)
	}
	cl, err := dialer.Dial(config.MPD.Network, config.MPD.Addr, "")
	if err != nil {
		log.Fatalf("failed to dial mpd: %v", err)
	}
	w, err := dialer.NewWatcher(config.MPD.Network, config.MPD.Addr, "")
	if err != nil {
		log.Fatalf("failed to dial mpd: %v", err)
	}
	commands, err := cl.Commands(ctx)
	if err != nil {
		log.Fatalf("failed to check mpd supported functions: %v", err)
	}
	// get music dir from local mpd connection
	if config.MPD.Network == "unix" && config.MPD.MusicDirectory == "" {
		if c, err := cl.Config(ctx); err == nil {
			if dir, ok := c["music_directory"]; ok && filepath.IsAbs(dir) {
				config.MPD.MusicDirectory = dir
				log.Printf("apply mpd.music_directory from mpd connection: %s", dir)
			}
		}
	}

	// get music dir from local mpd config
	mpdConf, _ := mpd.ParseConfig(config.MPD.Conf)
	if config.MPD.MusicDirectory == "" {
		if mpdConf != nil && filepath.IsAbs(config.MPD.Conf) {
			config.MPD.MusicDirectory = mpdConf.MusicDirectory
			log.Printf("apply mpd.music_directory from %s: %s", config.MPD.Conf, mpdConf.MusicDirectory)
		}
	}
	proxy := map[string]string{}
	if mpdConf != nil {
		host := "localhost"
		if config.MPD.Network == "tcp" {
			h := strings.Split(config.MPD.Addr, ":")[0]
			if len(h) != 0 {
				host = h
			}
		}
		for _, dev := range mpdConf.AudioOutputs {
			if len(dev.Port) != 0 {
				proxy[dev.Name] = "http://" + host + ":" + dev.Port
			}
		}
	}
	m := http.NewServeMux()
	covers := make([]cover.Cover, 0, 2)
	if config.Server.Cover.Local {
		if len(config.MPD.MusicDirectory) == 0 {
			log.Println("config.server.cover.local is disabled: mpd.music_directory is empty")
		} else {
			c, err := cover.NewLocal("/api/music/images/local/", config.MPD.MusicDirectory, []string{"cover.jpg", "cover.jpeg", "cover.png", "cover.gif", "cover.bmp"})
			if err != nil {
				log.Fatalf("failed to initialize coverart: %v", err)
			}
			m.Handle("/api/music/images/local/", c)
			covers = append(covers, c)

		}
	}
	if config.Server.Cover.Remote {
		if !contains(commands, "albumart") {
			log.Println("config.server.cover.remote is disabled: mpd does not support albumart command")
		} else {
			c, err := cover.NewRemote("/api/music/images/remote/", cl, filepath.Join(config.Server.CacheDirectory, "imgcache"))
			if err != nil {
				log.Fatalf("failed to initialize coverart: %v", err)
			}
			m.Handle("/api/music/images/remote/", c)
			covers = append(covers, c)
			defer c.Close()
		}
	}
	batch := cover.NewBatch(covers)
	assets := AssetsConfig{
		LocalAssets: config.debug,
		Extra: map[string]string{
			"AssetsAppCSSHash": string(AssetsAppCSSHash),
			"AssetsAppJSHash":  string(AssetsAppJSHash),
			"TREE":             string(tree),
			"TREE_ORDER":       string(treeOrder),
		},
		ExtraDate: date,
	}.NewAssetsHandler()
	api, stopAPI, err := APIConfig{
		AudioProxy: proxy,
	}.NewAPIHandler(ctx, cl, w, batch)
	if err != nil {
		log.Fatalf("failed to initialize api handler: %v", err)
	}
	m.Handle("/", assets)
	m.Handle("/api/", api)

	if err != nil {
		log.Fatalf("failed to initialize app: %v", err)
	}
	s := http.Server{
		Handler: m,
		Addr:    config.Server.Addr,
	}
	s.RegisterOnShutdown(stopAPI)
	errs := make(chan error, 1)
	go func() {
		errs <- s.ListenAndServe()
	}()
	sc := make(chan os.Signal, 1)
	signal.Notify(sc, syscall.SIGTERM, syscall.SIGINT)
	select {
	case <-sc:
	case err := <-errs:
		if err != http.ErrServerClosed {
			log.Fatalf("server stopped with error: %v", err)
		}
	}

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if err := s.Shutdown(ctx); err != nil {
		log.Printf("failed to stop http server: %v", err)
	}
	if err := cl.Close(ctx); err != nil {
		log.Printf("failed to close mpd connection(main): %v", err)
	}
	if err := w.Close(ctx); err != nil {
		log.Printf("failed to close mpd connection(event): %v", err)
	}
	if err := batch.Shutdown(ctx); err != nil {
		log.Printf("failed to stop image api: %v", err)
	}
}

func contains(list []string, item string) bool {
	for _, n := range list {
		if item == n {
			return true
		}
	}
	return false
}
<|endoftext|>


<commit_before>package main

import (
	"fmt"
	"net/http"
	"os"
	"strings"
	"time"

	log "github.com/Sirupsen/logrus"
	"github.com/Sirupsen/logrus/hooks/sentry"
	"github.com/facebookgo/inject"
	"github.com/robfig/cron"

	"github.com/oursky/skygear/asset"
	"github.com/oursky/skygear/authtoken"
	"github.com/oursky/skygear/handler"
	"github.com/oursky/skygear/plugin"
	_ "github.com/oursky/skygear/plugin/exec"
	"github.com/oursky/skygear/plugin/hook"
	_ "github.com/oursky/skygear/plugin/http"
	"github.com/oursky/skygear/plugin/provider"
	_ "github.com/oursky/skygear/plugin/zmq"
	pp "github.com/oursky/skygear/preprocessor"
	"github.com/oursky/skygear/pubsub"
	"github.com/oursky/skygear/push"
	"github.com/oursky/skygear/router"
	"github.com/oursky/skygear/skyconfig"
	"github.com/oursky/skygear/skydb"
	_ "github.com/oursky/skygear/skydb/pq"
	"github.com/oursky/skygear/subscription"
)

func usage() {
	fmt.Println("Usage: skygear [<config file>]")
}

func main() {
	var configPath string
	if len(os.Args) < 2 {
		configPath = os.Getenv("OD_CONFIG")
		if configPath == "" {
			usage()
			return
		}
	} else {
		configPath = os.Args[1]
	}

	config := skyconfig.Configuration{}
	if err := skyconfig.ReadFileInto(&config, configPath); err != nil {
		fmt.Println(err.Error())
		return
	}

	initLogger(config)
	connOpener := ensureDB(config) // Fatal on DB failed

	// Init all the services
	r := router.NewRouter()
	pushSender := initPushSender(config, connOpener)

	tokenStore := authtoken.InitTokenStore(config.TokenStore.ImplName, config.TokenStore.Path)

	preprocessorRegistry := router.PreprocessorRegistry{}

	cronjob := cron.New()
	initContext := plugin.InitContext{
		Router:           r,
		Preprocessors:    preprocessorRegistry,
		HookRegistry:     hook.NewRegistry(),
		ProviderRegistry: provider.NewRegistry(),
		Scheduler:        cronjob,
		Config:           config,
	}

	internalHub := pubsub.NewHub()
	initSubscription(config, connOpener, internalHub, pushSender)
	initDevice(config, connOpener)

	// Preprocessor
	preprocessorRegistry["notification"] = &pp.NotificationPreprocessor{
		NotificationSender: pushSender,
	}
	preprocessorRegistry["accesskey"] = &pp.AccessKeyValidationPreprocessor{
		ClientKey: config.App.APIKey,
		MasterKey: config.App.MasterKey,
		AppName:   config.App.Name,
	}
	preprocessorRegistry["authenticator"] = &pp.UserAuthenticator{
		ClientKey:  config.App.APIKey,
		MasterKey:  config.App.MasterKey,
		AppName:    config.App.Name,
		TokenStore: tokenStore,
	}
	preprocessorRegistry["dbconn"] = &pp.ConnPreprocessor{
		AppName:       config.App.Name,
		AccessControl: config.App.AccessControl,
		DBOpener:      skydb.Open,
		DBImpl:        config.DB.ImplName,
		Option:        config.DB.Option,
	}
	preprocessorRegistry["plugin"] = &pp.EnsurePluginReadyPreprocessor{&initContext}
	preprocessorRegistry["inject_user"] = &pp.InjectUserIfPresent{}
	preprocessorRegistry["require_user"] = &pp.RequireUserForWrite{}
	preprocessorRegistry["inject_db"] = &pp.InjectDatabase{}
	preprocessorRegistry["inject_public_db"] = &pp.InjectPublicDatabase{}
	preprocessorRegistry["dev_only"] = &pp.DevOnlyProcessor{config.App.DevMode}

	r.Map("", &handler.HomeHandler{})

	g := &inject.Graph{}
	injectErr := g.Provide(
		&inject.Object{Value: initContext.ProviderRegistry, Complete: true, Name: "ProviderRegistry"},
		&inject.Object{Value: initContext.HookRegistry, Complete: true, Name: "HookRegistry"},
		&inject.Object{Value: tokenStore, Complete: true, Name: "TokenStore"},
		&inject.Object{Value: initAssetStore(config), Complete: true, Name: "AssetStore"},
		&inject.Object{Value: pushSender, Complete: true, Name: "PushSender"},
		&inject.Object{
			Value:    skydb.GetAccessModel(config.App.AccessControl),
			Complete: true,
			Name:     "AccessModel",
		},
	)
	if injectErr != nil {
		panic(fmt.Sprintf("Unable to set up handler: %v", injectErr))
	}

	injector := router.HandlerInjector{
		g,
		&preprocessorRegistry,
	}

	r.Map("auth:signup", injector.Inject(&handler.SignupHandler{}))
	r.Map("auth:login", injector.Inject(&handler.LoginHandler{}))
	r.Map("auth:logout", injector.Inject(&handler.LogoutHandler{}))
	r.Map("auth:password", injector.Inject(&handler.PasswordHandler{}))

	r.Map("record:fetch", injector.Inject(&handler.RecordFetchHandler{}))
	r.Map("record:query", injector.Inject(&handler.RecordQueryHandler{}))
	r.Map("record:save", injector.Inject(&handler.RecordSaveHandler{}))
	r.Map("record:delete", injector.Inject(&handler.RecordDeleteHandler{}))

	r.Map("device:register", injector.Inject(&handler.DeviceRegisterHandler{}))

	// subscription shares the same set of preprocessor as record read at the moment
	r.Map("subscription:fetch_all", injector.Inject(&handler.SubscriptionFetchAllHandler{}))
	r.Map("subscription:delete", injector.Inject(&handler.SubscriptionDeleteHandler{}))

	// relation shares the same setof preprocessor
	r.Map("relation:query", injector.Inject(&handler.RelationQueryHandler{}))
	r.Map("relation:add", injector.Inject(&handler.RelationAddHandler{}))
	r.Map("relation:remove", injector.Inject(&handler.RelationRemoveHandler{}))

	r.Map("user:query", injector.Inject(&handler.UserQueryHandler{}))
	r.Map("user:update", injector.Inject(&handler.UserUpdateHandler{}))
	r.Map("user:link", injector.Inject(&handler.UserLinkHandler{}))

	r.Map("role:default", injector.Inject(&handler.RoleDefaultHandler{}))
	r.Map("role:admin", injector.Inject(&handler.RoleAdminHandler{}))

	r.Map("push:user", injector.Inject(&handler.PushToUserHandler{}))
	r.Map("push:device", injector.Inject(&handler.PushToDeviceHandler{}))

	r.Map("schema:rename", injector.Inject(&handler.SchemaRenameHandler{}))
	r.Map("schema:delete", injector.Inject(&handler.SchemaDeleteHandler{}))
	r.Map("schema:create", injector.Inject(&handler.SchemaCreateHandler{}))
	r.Map("schema:fetch", injector.Inject(&handler.SchemaFetchHandler{}))

	// Following section is for Gateway
	pubSub := pubsub.NewWsPubsub(nil)
	pubSubGateway := router.NewGateway(`pubSub`)
	pubSubGateway.GET(injector.InjectProcessors(&handler.PubSubHandler{
		WebSocket: pubSub,
	}))

	internalPubSub := pubsub.NewWsPubsub(internalHub)
	internalPubSubGateway := router.NewGateway(`internalpubSub`)
	internalPubSubGateway.GET(injector.InjectProcessors(&handler.PubSubHandler{
		WebSocket: internalPubSub,
	}))

	corsHost := config.App.CORSHost

	http.Handle("/", router.CORSMiddleware(
		router.LoggingMiddleware(r, false), corsHost))
	http.Handle("/pubsub", router.CORSMiddleware(
		router.LoggingMiddleware(pubSubGateway, false), corsHost))
	http.Handle("/_/pubsub", router.CORSMiddleware(
		router.LoggingMiddleware(internalPubSubGateway, false), corsHost))

	fileGateway := router.NewGateway(`files/(.+)`)
	fileGateway.GET(injector.Inject(&handler.AssetGetURLHandler{}))
	fileGateway.PUT(injector.Inject(&handler.AssetUploadURLHandler{}))
	http.Handle("/files/", router.CORSMiddleware(
		router.LoggingMiddleware(fileGateway, true), corsHost))

	// Bootstrap finished, starting services
	cronjob.Start()
	initPlugin(config, &initContext)

	log.Printf("Listening on %v...", config.HTTP.Host)
	err := http.ListenAndServe(config.HTTP.Host, nil)
	if err != nil {
		log.Printf("Failed: %v", err)
	}
}

func ensureDB(config skyconfig.Configuration) func() (skydb.Conn, error) {
	connOpener := func() (skydb.Conn, error) {
		return skydb.Open(
			config.DB.ImplName,
			config.App.Name,
			config.App.AccessControl,
			config.DB.Option,
		)
	}

	// Attempt to open connection to database. Retry for a number of
	// times before giving up.
	attempt := 0
	for {
		conn, connError := connOpener()
		if connError == nil {
			conn.Close()
			return connOpener
		}

		attempt++
		log.Errorf("Failed to start skygear: %v", connError)
		if attempt >= 5 {
			log.Fatalf("Failed to start skygear because connection to database cannot be opened.")
		}

		log.Info("Retrying in 1 second...")
		time.Sleep(time.Second * time.Duration(1))
	}
}

func initAssetStore(config skyconfig.Configuration) asset.Store {
	var store asset.Store
	switch config.AssetStore.ImplName {
	default:
		panic("unrecgonized asset store implementation: " + config.AssetStore.ImplName)
	case "fs":
		store = asset.NewFileStore(
			config.AssetStore.Path,
			config.AssetURLSigner.URLPrefix,
			config.AssetURLSigner.Secret,
			config.AssetStore.Public,
		)
	case "s3":
		s3Store, err := asset.NewS3Store(
			config.AssetStore.AccessToken,
			config.AssetStore.SecretToken,
			config.AssetStore.Region,
			config.AssetStore.Bucket,
			config.AssetStore.Public,
		)
		if err != nil {
			panic("failed to initialize asset.S3Store: " + err.Error())
		}
		store = s3Store
	}
	return store
}

func initDevice(config skyconfig.Configuration, connOpener func() (skydb.Conn, error)) {
	// TODO: Create a device service to check APNs to remove obsolete devices.
	// The current implementaion deletes pubsub devices if the last registered
	// time is more than 1 day old.
	conn, err := connOpener()
	if err != nil {
		log.Warnf("Failed to delete outdated devices: %v", err)
	}

	conn.DeleteEmptyDevicesByTime(time.Now().AddDate(0, 0, -1))
}

func initPushSender(config skyconfig.Configuration, connOpener func() (skydb.Conn, error)) push.Sender {
	routeSender := push.NewRouteSender()
	if config.APNS.Enable {
		apns := initAPNSPusher(config, connOpener)
		routeSender.Route("aps", apns)
		routeSender.Route("ios", apns)
	}
	if config.GCM.Enable {
		gcm := initGCMPusher(config)
		routeSender.Route("gcm", gcm)
		routeSender.Route("android", gcm)
	}
	return routeSender
}

func initAPNSPusher(config skyconfig.Configuration, connOpener func() (skydb.Conn, error)) *push.APNSPusher {
	apnsPushSender, err := push.NewAPNSPusher(connOpener, push.GatewayType(config.APNS.Env), config.APNS.Cert, config.APNS.Key)
	if err != nil {
		log.Fatalf("Failed to set up push sender: %v", err)
	}
	go apnsPushSender.Run()
	go apnsPushSender.RunFeedback()

	return apnsPushSender
}

func initGCMPusher(config skyconfig.Configuration) *push.GCMPusher {
	return &push.GCMPusher{APIKey: config.GCM.APIKey}
}

func initSubscription(config skyconfig.Configuration, connOpener func() (skydb.Conn, error), hub *pubsub.Hub, pushSender push.Sender) {
	notifiers := []subscription.Notifier{subscription.NewHubNotifier(hub)}
	if pushSender != nil {
		notifiers = append(notifiers, subscription.NewPushNotifier(pushSender))
	}

	subscriptionService := &subscription.Service{
		ConnOpener: connOpener,
		Notifier:   subscription.NewMultiNotifier(notifiers...),
	}
	log.Infoln("Subscription Service listening...")
	go subscriptionService.Run()
}

func initPlugin(config skyconfig.Configuration, initContext *plugin.InitContext) {
	log.Infof("Supported plugin transports: %s", strings.Join(plugin.SupportedTransports(), ", "))

	for _, pluginConfig := range config.Plugin {
		initContext.AddPluginConfiguration(pluginConfig.Transport, pluginConfig.Path, pluginConfig.Args)
	}

	initContext.InitPlugins()
}

func initLogger(config skyconfig.Configuration) {
	// Setup Logging
	log.SetOutput(os.Stderr)
	level, err := log.ParseLevel(config.LOG.Level)
	if err != nil {
		log.Warnf("log: error parsing config: %v", err)
		log.Warnln("log: fall back to `debug`")
		level = log.DebugLevel
	}
	log.SetLevel(level)

	if config.LogHook.SentryDSN != "" {
		initSentry(config)
	}
}

func higherLogLevels(minLevel log.Level) []log.Level {
	levels := []log.Level{
		log.PanicLevel,
		log.FatalLevel,
		log.ErrorLevel,
		log.WarnLevel,
		log.InfoLevel,
		log.DebugLevel,
	}

	output := make([]log.Level, 0, len(levels))
	for _, level := range levels {
		if level <= minLevel {
			output = append(output, level)
		}
	}
	return output
}

func initSentry(config skyconfig.Configuration) {
	level, err := log.ParseLevel(config.LogHook.SentryLevel)
	if err != nil {
		log.Fatalf("log-hook: error parsing sentry-level: %v", err)
		return
	}

	levels := higherLogLevels(level)

	hook, err := logrus_sentry.NewSentryHook(config.LogHook.SentryDSN, levels)
	hook.Timeout = 1 * time.Second
	if err != nil {
		log.Errorf("Failed to initialize Sentry: %v", err)
		return
	}
	log.Infof("Logging to Sentry: %v", levels)
	log.AddHook(hook)
}
<commit_msg>Bring back subscription:{save,fetch}<commit_after>package main

import (
	"fmt"
	"net/http"
	"os"
	"strings"
	"time"

	log "github.com/Sirupsen/logrus"
	"github.com/Sirupsen/logrus/hooks/sentry"
	"github.com/facebookgo/inject"
	"github.com/robfig/cron"

	"github.com/oursky/skygear/asset"
	"github.com/oursky/skygear/authtoken"
	"github.com/oursky/skygear/handler"
	"github.com/oursky/skygear/plugin"
	_ "github.com/oursky/skygear/plugin/exec"
	"github.com/oursky/skygear/plugin/hook"
	_ "github.com/oursky/skygear/plugin/http"
	"github.com/oursky/skygear/plugin/provider"
	_ "github.com/oursky/skygear/plugin/zmq"
	pp "github.com/oursky/skygear/preprocessor"
	"github.com/oursky/skygear/pubsub"
	"github.com/oursky/skygear/push"
	"github.com/oursky/skygear/router"
	"github.com/oursky/skygear/skyconfig"
	"github.com/oursky/skygear/skydb"
	_ "github.com/oursky/skygear/skydb/pq"
	"github.com/oursky/skygear/subscription"
)

func usage() {
	fmt.Println("Usage: skygear [<config file>]")
}

func main() {
	var configPath string
	if len(os.Args) < 2 {
		configPath = os.Getenv("OD_CONFIG")
		if configPath == "" {
			usage()
			return
		}
	} else {
		configPath = os.Args[1]
	}

	config := skyconfig.Configuration{}
	if err := skyconfig.ReadFileInto(&config, configPath); err != nil {
		fmt.Println(err.Error())
		return
	}

	initLogger(config)
	connOpener := ensureDB(config) // Fatal on DB failed

	// Init all the services
	r := router.NewRouter()
	pushSender := initPushSender(config, connOpener)

	tokenStore := authtoken.InitTokenStore(config.TokenStore.ImplName, config.TokenStore.Path)

	preprocessorRegistry := router.PreprocessorRegistry{}

	cronjob := cron.New()
	initContext := plugin.InitContext{
		Router:           r,
		Preprocessors:    preprocessorRegistry,
		HookRegistry:     hook.NewRegistry(),
		ProviderRegistry: provider.NewRegistry(),
		Scheduler:        cronjob,
		Config:           config,
	}

	internalHub := pubsub.NewHub()
	initSubscription(config, connOpener, internalHub, pushSender)
	initDevice(config, connOpener)

	// Preprocessor
	preprocessorRegistry["notification"] = &pp.NotificationPreprocessor{
		NotificationSender: pushSender,
	}
	preprocessorRegistry["accesskey"] = &pp.AccessKeyValidationPreprocessor{
		ClientKey: config.App.APIKey,
		MasterKey: config.App.MasterKey,
		AppName:   config.App.Name,
	}
	preprocessorRegistry["authenticator"] = &pp.UserAuthenticator{
		ClientKey:  config.App.APIKey,
		MasterKey:  config.App.MasterKey,
		AppName:    config.App.Name,
		TokenStore: tokenStore,
	}
	preprocessorRegistry["dbconn"] = &pp.ConnPreprocessor{
		AppName:       config.App.Name,
		AccessControl: config.App.AccessControl,
		DBOpener:      skydb.Open,
		DBImpl:        config.DB.ImplName,
		Option:        config.DB.Option,
	}
	preprocessorRegistry["plugin"] = &pp.EnsurePluginReadyPreprocessor{&initContext}
	preprocessorRegistry["inject_user"] = &pp.InjectUserIfPresent{}
	preprocessorRegistry["require_user"] = &pp.RequireUserForWrite{}
	preprocessorRegistry["inject_db"] = &pp.InjectDatabase{}
	preprocessorRegistry["inject_public_db"] = &pp.InjectPublicDatabase{}
	preprocessorRegistry["dev_only"] = &pp.DevOnlyProcessor{config.App.DevMode}

	r.Map("", &handler.HomeHandler{})

	g := &inject.Graph{}
	injectErr := g.Provide(
		&inject.Object{Value: initContext.ProviderRegistry, Complete: true, Name: "ProviderRegistry"},
		&inject.Object{Value: initContext.HookRegistry, Complete: true, Name: "HookRegistry"},
		&inject.Object{Value: tokenStore, Complete: true, Name: "TokenStore"},
		&inject.Object{Value: initAssetStore(config), Complete: true, Name: "AssetStore"},
		&inject.Object{Value: pushSender, Complete: true, Name: "PushSender"},
		&inject.Object{
			Value:    skydb.GetAccessModel(config.App.AccessControl),
			Complete: true,
			Name:     "AccessModel",
		},
	)
	if injectErr != nil {
		panic(fmt.Sprintf("Unable to set up handler: %v", injectErr))
	}

	injector := router.HandlerInjector{
		g,
		&preprocessorRegistry,
	}

	r.Map("auth:signup", injector.Inject(&handler.SignupHandler{}))
	r.Map("auth:login", injector.Inject(&handler.LoginHandler{}))
	r.Map("auth:logout", injector.Inject(&handler.LogoutHandler{}))
	r.Map("auth:password", injector.Inject(&handler.PasswordHandler{}))

	r.Map("record:fetch", injector.Inject(&handler.RecordFetchHandler{}))
	r.Map("record:query", injector.Inject(&handler.RecordQueryHandler{}))
	r.Map("record:save", injector.Inject(&handler.RecordSaveHandler{}))
	r.Map("record:delete", injector.Inject(&handler.RecordDeleteHandler{}))

	r.Map("device:register", injector.Inject(&handler.DeviceRegisterHandler{}))

	// subscription shares the same set of preprocessor as record read at the moment
	r.Map("subscription:fetch_all", injector.Inject(&handler.SubscriptionFetchAllHandler{}))
	r.Map("subscription:fetch", injector.Inject(&handler.SubscriptionFetchHandler{}))
	r.Map("subscription:save", injector.Inject(&handler.SubscriptionSaveHandler{}))
	r.Map("subscription:delete", injector.Inject(&handler.SubscriptionDeleteHandler{}))

	// relation shares the same setof preprocessor
	r.Map("relation:query", injector.Inject(&handler.RelationQueryHandler{}))
	r.Map("relation:add", injector.Inject(&handler.RelationAddHandler{}))
	r.Map("relation:remove", injector.Inject(&handler.RelationRemoveHandler{}))

	r.Map("user:query", injector.Inject(&handler.UserQueryHandler{}))
	r.Map("user:update", injector.Inject(&handler.UserUpdateHandler{}))
	r.Map("user:link", injector.Inject(&handler.UserLinkHandler{}))

	r.Map("role:default", injector.Inject(&handler.RoleDefaultHandler{}))
	r.Map("role:admin", injector.Inject(&handler.RoleAdminHandler{}))

	r.Map("push:user", injector.Inject(&handler.PushToUserHandler{}))
	r.Map("push:device", injector.Inject(&handler.PushToDeviceHandler{}))

	r.Map("schema:rename", injector.Inject(&handler.SchemaRenameHandler{}))
	r.Map("schema:delete", injector.Inject(&handler.SchemaDeleteHandler{}))
	r.Map("schema:create", injector.Inject(&handler.SchemaCreateHandler{}))
	r.Map("schema:fetch", injector.Inject(&handler.SchemaFetchHandler{}))

	// Following section is for Gateway
	pubSub := pubsub.NewWsPubsub(nil)
	pubSubGateway := router.NewGateway(`pubSub`)
	pubSubGateway.GET(injector.InjectProcessors(&handler.PubSubHandler{
		WebSocket: pubSub,
	}))

	internalPubSub := pubsub.NewWsPubsub(internalHub)
	internalPubSubGateway := router.NewGateway(`internalpubSub`)
	internalPubSubGateway.GET(injector.InjectProcessors(&handler.PubSubHandler{
		WebSocket: internalPubSub,
	}))

	corsHost := config.App.CORSHost

	http.Handle("/", router.CORSMiddleware(
		router.LoggingMiddleware(r, false), corsHost))
	http.Handle("/pubsub", router.CORSMiddleware(
		router.LoggingMiddleware(pubSubGateway, false), corsHost))
	http.Handle("/_/pubsub", router.CORSMiddleware(
		router.LoggingMiddleware(internalPubSubGateway, false), corsHost))

	fileGateway := router.NewGateway(`files/(.+)`)
	fileGateway.GET(injector.Inject(&handler.AssetGetURLHandler{}))
	fileGateway.PUT(injector.Inject(&handler.AssetUploadURLHandler{}))
	http.Handle("/files/", router.CORSMiddleware(
		router.LoggingMiddleware(fileGateway, true), corsHost))

	// Bootstrap finished, starting services
	cronjob.Start()
	initPlugin(config, &initContext)

	log.Printf("Listening on %v...", config.HTTP.Host)
	err := http.ListenAndServe(config.HTTP.Host, nil)
	if err != nil {
		log.Printf("Failed: %v", err)
	}
}

func ensureDB(config skyconfig.Configuration) func() (skydb.Conn, error) {
	connOpener := func() (skydb.Conn, error) {
		return skydb.Open(
			config.DB.ImplName,
			config.App.Name,
			config.App.AccessControl,
			config.DB.Option,
		)
	}

	// Attempt to open connection to database. Retry for a number of
	// times before giving up.
	attempt := 0
	for {
		conn, connError := connOpener()
		if connError == nil {
			conn.Close()
			return connOpener
		}

		attempt++
		log.Errorf("Failed to start skygear: %v", connError)
		if attempt >= 5 {
			log.Fatalf("Failed to start skygear because connection to database cannot be opened.")
		}

		log.Info("Retrying in 1 second...")
		time.Sleep(time.Second * time.Duration(1))
	}
}

func initAssetStore(config skyconfig.Configuration) asset.Store {
	var store asset.Store
	switch config.AssetStore.ImplName {
	default:
		panic("unrecgonized asset store implementation: " + config.AssetStore.ImplName)
	case "fs":
		store = asset.NewFileStore(
			config.AssetStore.Path,
			config.AssetURLSigner.URLPrefix,
			config.AssetURLSigner.Secret,
			config.AssetStore.Public,
		)
	case "s3":
		s3Store, err := asset.NewS3Store(
			config.AssetStore.AccessToken,
			config.AssetStore.SecretToken,
			config.AssetStore.Region,
			config.AssetStore.Bucket,
			config.AssetStore.Public,
		)
		if err != nil {
			panic("failed to initialize asset.S3Store: " + err.Error())
		}
		store = s3Store
	}
	return store
}

func initDevice(config skyconfig.Configuration, connOpener func() (skydb.Conn, error)) {
	// TODO: Create a device service to check APNs to remove obsolete devices.
	// The current implementaion deletes pubsub devices if the last registered
	// time is more than 1 day old.
	conn, err := connOpener()
	if err != nil {
		log.Warnf("Failed to delete outdated devices: %v", err)
	}

	conn.DeleteEmptyDevicesByTime(time.Now().AddDate(0, 0, -1))
}

func initPushSender(config skyconfig.Configuration, connOpener func() (skydb.Conn, error)) push.Sender {
	routeSender := push.NewRouteSender()
	if config.APNS.Enable {
		apns := initAPNSPusher(config, connOpener)
		routeSender.Route("aps", apns)
		routeSender.Route("ios", apns)
	}
	if config.GCM.Enable {
		gcm := initGCMPusher(config)
		routeSender.Route("gcm", gcm)
		routeSender.Route("android", gcm)
	}
	return routeSender
}

func initAPNSPusher(config skyconfig.Configuration, connOpener func() (skydb.Conn, error)) *push.APNSPusher {
	apnsPushSender, err := push.NewAPNSPusher(connOpener, push.GatewayType(config.APNS.Env), config.APNS.Cert, config.APNS.Key)
	if err != nil {
		log.Fatalf("Failed to set up push sender: %v", err)
	}
	go apnsPushSender.Run()
	go apnsPushSender.RunFeedback()

	return apnsPushSender
}

func initGCMPusher(config skyconfig.Configuration) *push.GCMPusher {
	return &push.GCMPusher{APIKey: config.GCM.APIKey}
}

func initSubscription(config skyconfig.Configuration, connOpener func() (skydb.Conn, error), hub *pubsub.Hub, pushSender push.Sender) {
	notifiers := []subscription.Notifier{subscription.NewHubNotifier(hub)}
	if pushSender != nil {
		notifiers = append(notifiers, subscription.NewPushNotifier(pushSender))
	}

	subscriptionService := &subscription.Service{
		ConnOpener: connOpener,
		Notifier:   subscription.NewMultiNotifier(notifiers...),
	}
	log.Infoln("Subscription Service listening...")
	go subscriptionService.Run()
}

func initPlugin(config skyconfig.Configuration, initContext *plugin.InitContext) {
	log.Infof("Supported plugin transports: %s", strings.Join(plugin.SupportedTransports(), ", "))

	for _, pluginConfig := range config.Plugin {
		initContext.AddPluginConfiguration(pluginConfig.Transport, pluginConfig.Path, pluginConfig.Args)
	}

	initContext.InitPlugins()
}

func initLogger(config skyconfig.Configuration) {
	// Setup Logging
	log.SetOutput(os.Stderr)
	level, err := log.ParseLevel(config.LOG.Level)
	if err != nil {
		log.Warnf("log: error parsing config: %v", err)
		log.Warnln("log: fall back to `debug`")
		level = log.DebugLevel
	}
	log.SetLevel(level)

	if config.LogHook.SentryDSN != "" {
		initSentry(config)
	}
}

func higherLogLevels(minLevel log.Level) []log.Level {
	levels := []log.Level{
		log.PanicLevel,
		log.FatalLevel,
		log.ErrorLevel,
		log.WarnLevel,
		log.InfoLevel,
		log.DebugLevel,
	}

	output := make([]log.Level, 0, len(levels))
	for _, level := range levels {
		if level <= minLevel {
			output = append(output, level)
		}
	}
	return output
}

func initSentry(config skyconfig.Configuration) {
	level, err := log.ParseLevel(config.LogHook.SentryLevel)
	if err != nil {
		log.Fatalf("log-hook: error parsing sentry-level: %v", err)
		return
	}

	levels := higherLogLevels(level)

	hook, err := logrus_sentry.NewSentryHook(config.LogHook.SentryDSN, levels)
	hook.Timeout = 1 * time.Second
	if err != nil {
		log.Errorf("Failed to initialize Sentry: %v", err)
		return
	}
	log.Infof("Logging to Sentry: %v", levels)
	log.AddHook(hook)
}
<|endoftext|>


<commit_before>/*
This package implements the challenge for SD Gophers.

The general algorithm for this is going to be the following:

We are going to only do this for 2x2 or bigger matrix. For things less then
that we will treat it as a special case.

we will start with the following matrix:

  3 2
  4 1

If it's a 2x2 we are done.
For a 3 x 3 we will do the following:
First add a new row

   3 2
   4 1
   5 6  // This is the new row added.

Next we will flip row and columns.

   5 4 3
   6 1 2

Next we add another row.

   5 4 3
   6 1 2
   7 8 9

Now we print the Matrix.

*/
package main

import (
	"flag"
	"fmt"
	"log"
	"os"
	"runtime/pprof"
	"strconv"
)

var dflag = flag.Uint("d", 2, "The dimentions of the square.")
var sflag = flag.Uint("s", 1, "The dimentions of the square.")
var memprofile = flag.String("memprofile", "", "write memory profile to this file")
var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file")

const (
	StartSize = 2
)

type Sprial struct {
	TargetSize  int
	StartOffset uint
	s           [][]uint
}

func (s *Sprial) Init() {
	if s.TargetSize < StartSize {
		s.TargetSize = StartSize
	}
	m := make([][]uint, StartSize, s.TargetSize)
	for i := 0; i < len(m); i++ {
		m[i] = make([]uint, StartSize, s.TargetSize)
	}
	m[0][0] = 3 + s.StartOffset
	m[0][1] = 2 + s.StartOffset
	m[1][0] = 4 + s.StartOffset
	m[1][1] = 1 + s.StartOffset
	s.s = m
}

func (s Sprial) largestValue() uint {
	// The largest value in the sprial will always be kept in first column of
	// the last row.
	return s.s[len(s.s)-1][0]
}

// addNextRow add next next row of numbers to the bottom of the sprial. This function assumes that the
// largest number is in the first column of the last row.
func (s *Sprial) addNextRow() {
	if s.s == nil || len(s.s) == 0 {
		s.Init()
	}

	start := s.largestValue() + 1 // Starting value
	row := make([]uint, len(s.s[0]), s.TargetSize)
	for i := 0; i < len(s.s[0]); i++ {
		row[i] = uint(i) + start
	}
	s.s = append(s.s, row)
}

// rotate roates the sprial so that the largest number is in the first column of the last row.
func (s *Sprial) rotate() {
	// Make a new sprial we are going to copy the number from the original into the correct spots
	// into this new sprial. this new sprial will be N x M where the original sprial was M x N.
	ns := make([][]uint, len(s.s[0]), s.TargetSize)
	for i := 0; i < len(ns); i++ {
		ns[i] = make([]uint, len(s.s), s.TargetSize)
	}
	slen := len(s.s) - 1
	for i := 0; i < len(s.s[0]); i++ {
		for j := slen; j >= 0; j-- {
			ns[i][slen-j] = s.s[j][i]
		}
	}
	s.s = ns
}

func (s *Sprial) AddNextRow() {
	s.addNextRow()
	// We want to leave it so that the m[0][len(m[0])-1] is the largest number in the spiral. This way we can call addNextRow without worrying about the orintation of the square.
	s.rotate()
}

func (s Sprial) String() string {
	if s.TargetSize < 2 {
		if s.TargetSize == 1 {
			return "1"
		}
		return ""
	}
	if s.s == nil {
		s.Init()
		for i := 0; i < s.TargetSize-StartSize; i++ {
			s.AddNextRow()
			s.AddNextRow()
		}
	}
	str := strconv.Itoa(int(s.largestValue()))
	format := fmt.Sprintf("%%0.%dd ", len(str))
	str = ""
	for _, r := range s.s {
		for _, c := range r {
			str += fmt.Sprintf(format, c)
		}
		str += fmt.Sprintf("\n")
	}
	return str
}

func main() {
	flag.Parse()
	var offset uint
	if *sflag > 0 {
		offset = *sflag - 1
	}
	if *cpuprofile != "" {
		f, err := os.Create(*cpuprofile)
		if err != nil {
			log.Fatal(err)
		}
		pprof.StartCPUProfile(f)
		defer pprof.StopCPUProfile()
	}
	s := Sprial{TargetSize: int(*dflag), StartOffset: offset}
	fmt.Printf("%v\n", s)
	if *memprofile != "" {
		f, err := os.Create(*memprofile)
		if err != nil {
			log.Fatal(err)
		}
		pprof.WriteHeapProfile(f)
		f.Close()
		return
	}
}
<commit_msg>Fix the documentation.<commit_after>/*
This package implements the challenge for SD Gophers.

The general algorithm for this is going to be the following:

We are going to only do this for 2x2 or bigger matrix. For things less then
that we will treat it as a special case.

we will start with the following matrix:

  3 2
  4 1

If it's a 2x2 we are done.
For a 3 x 3 we will do the following:
First add a new row

   3 2
   4 1
   5 6  // This is the new row added.

Next we will flip row and columns.

   5 4 3
   6 1 2

Next we add another row.

   5 4 3
   6 1 2
   7 8 9

Now we rotate the matrix, so that the largest number
is in the last row of the first column. This way we can
repeate the steps; if we need a larger matrix.

   7 6 5
	8 1 4
	9 2 3

Now we print the Matrix.

*/
package main

import (
	"flag"
	"fmt"
	"log"
	"os"
	"runtime/pprof"
	"strconv"
)

var dflag = flag.Uint("d", 2, "The dimentions of the square.")
var sflag = flag.Uint("s", 1, "The dimentions of the square.")
var memprofile = flag.String("memprofile", "", "write memory profile to this file")
var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file")

const (
	StartSize = 2
)

type Sprial struct {
	TargetSize  int
	StartOffset uint
	s           [][]uint
}

func (s *Sprial) Init() {
	if s.TargetSize < StartSize {
		s.TargetSize = StartSize
	}
	m := make([][]uint, StartSize, s.TargetSize)
	for i := 0; i < len(m); i++ {
		m[i] = make([]uint, StartSize, s.TargetSize)
	}
	m[0][0] = 3 + s.StartOffset
	m[0][1] = 2 + s.StartOffset
	m[1][0] = 4 + s.StartOffset
	m[1][1] = 1 + s.StartOffset
	s.s = m
}

func (s Sprial) largestValue() uint {
	// The largest value in the sprial will always be kept in first column of
	// the last row.
	return s.s[len(s.s)-1][0]
}

// addNextRow add next next row of numbers to the bottom of the sprial. This function assumes that the
// largest number is in the first column of the last row.
func (s *Sprial) addNextRow() {
	if s.s == nil || len(s.s) == 0 {
		s.Init()
	}

	start := s.largestValue() + 1 // Starting value
	row := make([]uint, len(s.s[0]), s.TargetSize)
	for i := 0; i < len(s.s[0]); i++ {
		row[i] = uint(i) + start
	}
	s.s = append(s.s, row)
}

// rotate roates the sprial so that the largest number is in the first column of the last row.
func (s *Sprial) rotate() {
	// Make a new sprial we are going to copy the number from the original into the correct spots
	// into this new sprial. this new sprial will be N x M where the original sprial was M x N.
	ns := make([][]uint, len(s.s[0]), s.TargetSize)
	for i := 0; i < len(ns); i++ {
		ns[i] = make([]uint, len(s.s), s.TargetSize)
	}
	slen := len(s.s) - 1
	for i := 0; i < len(s.s[0]); i++ {
		for j := slen; j >= 0; j-- {
			ns[i][slen-j] = s.s[j][i]
		}
	}
	s.s = ns
}

func (s *Sprial) AddNextRow() {
	s.addNextRow()
	// We want to leave it so that the m[0][len(m[0])-1] is the largest number in the spiral. This way we can call addNextRow without worrying about the orintation of the square.
	s.rotate()
}

func (s Sprial) String() string {
	if s.TargetSize < 2 {
		if s.TargetSize == 1 {
			return "1"
		}
		return ""
	}
	if s.s == nil {
		s.Init()
		for i := 0; i < s.TargetSize-StartSize; i++ {
			s.AddNextRow()
			s.AddNextRow()
		}
	}
	str := strconv.Itoa(int(s.largestValue()))
	format := fmt.Sprintf("%%0.%dd ", len(str))
	str = ""
	for _, r := range s.s {
		for _, c := range r {
			str += fmt.Sprintf(format, c)
		}
		str += fmt.Sprintf("\n")
	}
	return str
}

func main() {
	flag.Parse()
	var offset uint
	if *sflag > 0 {
		offset = *sflag - 1
	}
	if *cpuprofile != "" {
		f, err := os.Create(*cpuprofile)
		if err != nil {
			log.Fatal(err)
		}
		pprof.StartCPUProfile(f)
		defer pprof.StopCPUProfile()
	}
	s := Sprial{TargetSize: int(*dflag), StartOffset: offset}
	fmt.Printf("%v\n", s)
	if *memprofile != "" {
		f, err := os.Create(*memprofile)
		if err != nil {
			log.Fatal(err)
		}
		pprof.WriteHeapProfile(f)
		f.Close()
		return
	}
}
<|endoftext|>


<commit_before>package main

// Copyright 2015 MediaMath <http://www.mediamath.com>.  All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

import (
	"flag"
	"fmt"
	"log"
	"os"
	"strings"
	"time"
)

//examples:
// - ARTIFACTORY_HOST=https://example.com ARTIFACTORY_USER=foo ARTIFACTORY_PASSWORD=bar part -g com.example -a goo -v 1.2 -r local-release goo-jdk1.2.zip
// - part -c example.com.json -g com.example -a goo -v 1.2-SNAPSHOT -r local-snapshot goo.zip

const hostEnvVariable = "ARTIFACTORY_HOST"

var (
	verbose = flag.Bool("verbose", false, "Show verbose output.")
	getFlag = flag.Bool("get", false, "Get the artifact instead of publishing it.")
	pomOnly = flag.Bool("pomOnly", false, "Do NOT publish.  Generate poms only")
	timeout = flag.String("t", "30s", "Client timeout")

	credentialsFile = flag.String("credentials", "", fmt.Sprintf("File with user, password.  If .json extension assumes json otherwise ini.  If not provided assumes %s, %s environment variables are provided.", userEnvVariable, passwordEnvVariable))
	host            = flag.String("h", os.Getenv(hostEnvVariable), fmt.Sprintf("Artifactory REST API endpoint (ie https://artifactory.example.com/artifactory/). If not provided looks at environment variable %s.", hostEnvVariable))
	repo            = flag.String("r", "", "Repository to publish to")
	group           = flag.String("g", "", "Maven group")
	artifact        = flag.String("a", "", "Maven artifact")
	version         = flag.String("v", "", "Maven version")
)

func parseLocations() ([]*location, error) {
	creds, err := getCredentials(*credentialsFile)
	if err != nil {
		return nil, err
	}

	if len(flag.Args()) < 1 {
		return nil, fmt.Errorf("Must provide something to publish")
	}

	if *host == "" ||
		*repo == "" ||
		*group == "" ||
		*version == "" {
		return nil, fmt.Errorf("Must provide all required fields")
	}

	artifacts := make(map[string][]string)
	if *artifact != "" {
		artifacts[*artifact] = flag.Args()
	} else {
		for _, colonDelimited := range flag.Args() {
			pair := strings.Split(colonDelimited, ":")
			if len(pair) != 2 {
				return nil, fmt.Errorf("Could not parse: %v", colonDelimited)
			}

			artifacts[pair[0]] = append(artifacts[pair[0]], pair[1])
		}
	}

	locations := []*location{}
	for artifact, files := range artifacts {
		for _, file := range files {

			loc := &location{}
			loc.creds = creds

			loc.host = *host
			loc.repo = *repo
			loc.group = *group
			loc.version = *version

			loc.artifact = artifact
			loc.file = file
		}
	}

	return locations, nil
}

func main() {
	flag.Parse()
	locations, err := parseLocations()

	if err != nil {
		flag.PrintDefaults()
		log.Fatal(err)
	}

	timeoutDuration, parseErr := time.ParseDuration(*timeout)
	if parseErr != nil {
		log.Printf("Cannout parse timeout, using 30s: %v", parseErr)
		timeoutDuration = 30 * time.Second
	}

	if *getFlag {
		for _, loc := range locations {
			getErr := getArtifact(loc)
			if getErr != nil {
				log.Fatal(getErr)
			}
		}
	} else {

		for _, loc := range locations {
			fileResponse, pomResponse, publishErr := publish(timeoutDuration, *pomOnly, loc)

			if publishErr != nil {
				log.Fatal(publishErr)
			}

			fmt.Println(fileResponse.AsString(*verbose))
			fmt.Println(pomResponse.AsString(*verbose))
		}
	}
}
<commit_msg>fix bug where locations werent being run<commit_after>package main

// Copyright 2015 MediaMath <http://www.mediamath.com>.  All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

import (
	"flag"
	"fmt"
	"log"
	"os"
	"strings"
	"time"
)

//examples:
// - ARTIFACTORY_HOST=https://example.com ARTIFACTORY_USER=foo ARTIFACTORY_PASSWORD=bar part -g com.example -a goo -v 1.2 -r local-release goo-jdk1.2.zip
// - part -c example.com.json -g com.example -a goo -v 1.2-SNAPSHOT -r local-snapshot goo.zip

const hostEnvVariable = "ARTIFACTORY_HOST"

var (
	verbose = flag.Bool("verbose", false, "Show verbose output.")
	getFlag = flag.Bool("get", false, "Get the artifact instead of publishing it.")
	pomOnly = flag.Bool("pomOnly", false, "Do NOT publish.  Generate poms only")
	timeout = flag.String("t", "30s", "Client timeout")

	credentialsFile = flag.String("credentials", "", fmt.Sprintf("File with user, password.  If .json extension assumes json otherwise ini.  If not provided assumes %s, %s environment variables are provided.", userEnvVariable, passwordEnvVariable))
	host            = flag.String("h", os.Getenv(hostEnvVariable), fmt.Sprintf("Artifactory REST API endpoint (ie https://artifactory.example.com/artifactory/). If not provided looks at environment variable %s.", hostEnvVariable))
	repo            = flag.String("r", "", "Repository to publish to")
	group           = flag.String("g", "", "Maven group")
	artifact        = flag.String("a", "", "Maven artifact")
	version         = flag.String("v", "", "Maven version")
)

func parseLocations() ([]*location, error) {
	creds, err := getCredentials(*credentialsFile)
	if err != nil {
		return nil, err
	}

	if len(flag.Args()) < 1 {
		return nil, fmt.Errorf("Must provide something to publish")
	}

	if *host == "" ||
		*repo == "" ||
		*group == "" ||
		*version == "" {
		return nil, fmt.Errorf("Must provide all required fields")
	}

	artifacts := make(map[string][]string)
	if *artifact != "" {
		artifacts[*artifact] = flag.Args()
	} else {
		for _, colonDelimited := range flag.Args() {
			pair := strings.Split(colonDelimited, ":")
			if len(pair) != 2 {
				return nil, fmt.Errorf("Could not parse: %v", colonDelimited)
			}

			artifacts[pair[0]] = append(artifacts[pair[0]], pair[1])
		}
	}

	locations := []*location{}
	for artifact, files := range artifacts {
		for _, file := range files {

			loc := &location{}
			loc.creds = creds

			loc.host = *host
			loc.repo = *repo
			loc.group = *group
			loc.version = *version

			loc.artifact = artifact
			loc.file = file

			locations = append(locations, loc)
		}
	}

	return locations, nil
}

func main() {
	flag.Parse()
	locations, err := parseLocations()

	if err != nil {
		flag.PrintDefaults()
		log.Fatal(err)
	}

	timeoutDuration, parseErr := time.ParseDuration(*timeout)
	if parseErr != nil {
		log.Printf("Cannout parse timeout, using 30s: %v", parseErr)
		timeoutDuration = 30 * time.Second
	}

	if *getFlag {
		for _, loc := range locations {
			getErr := getArtifact(loc)
			if getErr != nil {
				log.Fatal(getErr)
			}
		}
	} else {

		for _, loc := range locations {
			fileResponse, pomResponse, publishErr := publish(timeoutDuration, *pomOnly, loc)

			if publishErr != nil {
				log.Fatal(publishErr)
			}

			fmt.Println(fileResponse.AsString(*verbose))
			fmt.Println(pomResponse.AsString(*verbose))
		}
	}
}
<|endoftext|>


<commit_before>package unfurl

import (
	"errors"
	"net/http"
	"net/http/cookiejar"
	"net/url"
)

// Client represents a unfurl client instance
type Client struct {
	httpClient *http.Client
	options    Options
}

// Options exposes internal settings to change the Client behaviour
type Options struct {
	// MaxHops defines how many redirects the client can suffer before returning
	// an error
	MaxHops int
	// UserAgent defines the UserAgent value used by the underlying http client
	UserAgent *string
}

// ErrTooManyRedirects indicates that the unfurl client has archieved the
// maximum allowed request count defined by the Options struct
var ErrTooManyRedirects = errors.New("Too many redirects")

// NewClientWithOptions returns a new instance of a Client using the provided
// Options values
func NewClientWithOptions(opts Options) Client {
	return Client{
		options: opts,
		httpClient: &http.Client{
			CheckRedirect: func(req *http.Request, via []*http.Request) error {
				return http.ErrUseLastResponse
			},
		},
	}
}

// NewClient returns a new instance of a Client using the default values for
// the settings parameters.
// (MaxHops: 20)
// (UserAgent: nil)
func NewClient() Client {
	return NewClientWithOptions(Options{MaxHops: 20})
}

// Process attempts to follow all possible redirects of a given URL
func (c *Client) Process(in string) (string, error) {
	jar, _ := cookiejar.New(nil)
	c.httpClient.Jar = jar
	hops := 0
	for {
		req, err := http.NewRequest("GET", in, nil)
		if err != nil {
			return "", err
		}
		if c.options.UserAgent != nil {
			req.Header.Set("User-Agent", *c.options.UserAgent)
		}
		resp, err := c.httpClient.Do(req)
		if err != nil {
			return "", err
		}
		if _, ok := resp.Header["Location"]; ok && resp.StatusCode/100.0 == 3 {
			if hops >= c.options.MaxHops {
				return "", ErrTooManyRedirects
			}
			hops++
			base, err := url.Parse(in)
			if err != nil {
				return "", err
			}
			next, err := url.Parse(resp.Header["Location"][0])
			if err != nil {
				return "", err
			}
			in = base.ResolveReference(next).String()
			continue
		} else {
			return resp.Request.URL.String(), nil
		}
	}
}
<commit_msg>Ensure to defer Body.Close() call<commit_after>package unfurl

import (
	"errors"
	"net/http"
	"net/http/cookiejar"
	"net/url"
)

// Client represents a unfurl client instance
type Client struct {
	httpClient *http.Client
	options    Options
}

// Options exposes internal settings to change the Client behaviour
type Options struct {
	// MaxHops defines how many redirects the client can suffer before returning
	// an error
	MaxHops int
	// UserAgent defines the UserAgent value used by the underlying http client
	UserAgent *string
}

// ErrTooManyRedirects indicates that the unfurl client has archieved the
// maximum allowed request count defined by the Options struct
var ErrTooManyRedirects = errors.New("Too many redirects")

// NewClientWithOptions returns a new instance of a Client using the provided
// Options values
func NewClientWithOptions(opts Options) Client {
	return Client{
		options: opts,
		httpClient: &http.Client{
			CheckRedirect: func(req *http.Request, via []*http.Request) error {
				return http.ErrUseLastResponse
			},
		},
	}
}

// NewClient returns a new instance of a Client using the default values for
// the settings parameters.
// (MaxHops: 20)
// (UserAgent: nil)
func NewClient() Client {
	return NewClientWithOptions(Options{MaxHops: 20})
}

// Process attempts to follow all possible redirects of a given URL
func (c *Client) Process(in string) (string, error) {
	jar, _ := cookiejar.New(nil)
	c.httpClient.Jar = jar
	hops := 0
	for {
		req, err := http.NewRequest("GET", in, nil)
		if err != nil {
			return "", err
		}
		if c.options.UserAgent != nil {
			req.Header.Set("User-Agent", *c.options.UserAgent)
		}
		resp, err := c.httpClient.Do(req)
		if err != nil {
			return "", err
		}
		defer resp.Body.Close()
		if _, ok := resp.Header["Location"]; ok && resp.StatusCode/100.0 == 3 {
			if hops >= c.options.MaxHops {
				return "", ErrTooManyRedirects
			}
			hops++
			base, err := url.Parse(in)
			if err != nil {
				return "", err
			}
			next, err := url.Parse(resp.Header["Location"][0])
			if err != nil {
				return "", err
			}
			in = base.ResolveReference(next).String()
			continue
		} else {
			return resp.Request.URL.String(), nil
		}
	}
}
<|endoftext|>