----
-- Example lsyncd configuration for syncing with an Amazon S3 bucket
--
-- This requires the official AWS CLI to be available, and that credentials
-- bet set up through some external method, such as environment variables,
-- IAM profiles or the AWS SDK configuration.
--
-- The AWS CLI sync exclude rules are not as powerful as the ones supported by
-- lsyncd. Hence, some of the do not translate perfectly. For example, '*'
-- (asterisk) matches slashes, while it does not in lsyncd. Hence it is a good
-- idea to only use exclude patterns for full directories, either by using a
-- trailing / (slash) or ** (double asterisk), as those will be correctly
-- translated.
--
-- An initialSync options is provided as a convenience, since it's not easy to
-- make sure exclusion rules match when doing it manually. It will *pull* from
-- the target bucket to the local dir (the opposite of the regular behavior)
-- then exit immediately.
--
-- Author: Daniel Miranda <danielkza2@gmail.com>
--
local s3 = {}

s3.checkgauge = {
    onCreate  = false,
    onModify  = false,
    onDelete  = false,
    onStartup = false,
    onMove    = false,

    delete      = true,
    exclude     = true,
    excludeFrom = true,
    target      = true,

    s3 = {
        -- Path to the AWS CLI binary
        awscliBinary  = true,
        -- Extra options to pass to the AWS CLI (as a list)
        awscliOptions = true,
        -- Whether to do a dry-run, and not make any real changes
        dryrun        = true,
        -- Do an initial pull from the bucket and exit immediately.
        initialSync   = true
    }
}

-- Generate a list of exclude flags for the AWS CLI  based on the lsyncd
-- patterns provided. Cache it to avoid re-generating it every time.

local s3Excludes = function(config, excludes)
    if config.s3._excludes == nil then
        config.s3._excludes = {}
        for _, pat in ipairs(excludes) do
            pat = pat:gsub('%*%*', '[[ANY]]')
            pat = pat:gsub('%?',   '[[ANY_BUT_SLASH_ONCE]]')
            pat = pat:gsub('/$',   '/*')
            pat = pat:gsub('%[%[ANY%]%]',                '*')
            pat = pat:gsub('%[%[ANY_BUT_SLASH_ONCE%]%]', '[^/]')

            if pat:match('^/') then
                pat = pat:sub(2, -1)
            else
                pat = '*/' .. pat
            end

            table.insert(config.s3._excludes, '--exclude')
            table.insert(config.s3._excludes, pat)
        end

        log('s3Excludes', table.concat(config.s3._excludes, '\n'))
    end

    return config.s3._excludes
end

-- Generates a command line to call the AWS CLI as configured, with the provided
-- S3 action (such as cp, mv, rm or sync).
-- Returns a tuple of (binaryPath, arguments)
local awscliCommand = function(verb, config)
    local bin = config.s3.awscliBinary
    local args = {'s3', verb, '--only-show-errors'}
    if config.s3.dryrun then
        table.insert(args, '--dryrun')
    end

    if verb == 'sync'
       and (config.delete == true or config.delete == 'startup')
    then
        table.insert(args, '--delete')
    end

    for _, opt in ipairs(config.s3.awscliOptions) do
        table.insert(args, opt)
    end

    return bin, args
end

s3.action = function(inlet)
    local event, event2 = inlet.getEvent()
    -- S3 never actually deals with directories - they are just an illusion
    -- created based on the common prefixes of objects. Hence discard any events
    -- that do not concern files.
    if event.isdir then
        inlet.discardEvent(event)
        return
    end

    local config = inlet.getConfig()
    if event.etype == 'Create' or event.etype == 'Modify' then
        local bin, args = awscliCommand('cp', config)
        spawn(
            event,
            bin,
            args,
            event.sourcePath,
            event.targetPath
        )
    elseif event.etype == 'Delete' then
        if config.delete ~= true and config.delete ~= 'running' then
            inlet.discardEvent(event)
            return
        end

        local bin, args = awscliCommand('rm', config)
        spawn(
            event,
            bin,
            args,
            event.targetPath
        )
    elseif event.etype == 'Move' then
        local bin, args = awscliCommand('mv', config)
        spawn(
            event,
            bin,
            args,
            event.targetPath,
            event2.targetPath
        )
    else
        log('Warn', 'ignored an event of type "', event.etype, '"')
        inlet.discardEvent(event)
    end
end

s3.init = function(event)
    local config = event.config
    local inlet = event.inlet
    local excludes = s3Excludes(config, inlet.getExcludes())
    local bin, args = awscliCommand('sync', config)

    -- Do a pull when initialSync is enabled.
    if config.s3.initialSync then
        spawn(
            event,
            bin,
            args,
            excludes,
            config.target,
            event.sourcePath
        )
    -- And a push, as usual, otherwise
    else
        spawn(
            event,
            bin,
            args,
            excludes,
            event.sourcePath,
            config.target
        )
    end
end

-- Define a collect callback so we can terminate immediately when initialSync
-- is enabled
s3.collect = function(agent, exitcode)
    local config = agent.config
    if not agent.isList and agent.etype == 'Init' and config.s3.initialSync then
        terminate(exitcode == 0 and 0 or -1)
    end

    return
end

s3.prepare = function(config, level)
    default.prepare(config, level + 1)

    config.target = config.target:gsub('/+$', '')
    if not config.target:match('^s3://') then
        config.target = 's3://' .. config.target
    end
end

s3.s3 = {
    awscliBinary  = '/usr/bin/aws',
    awscliOptions = {},
    dryrun       = false,
    initialSync  = false
}
s3.delete       = false
s3.delay        = 10
s3.maxProcesses = 1

sync {
    s3,
    source       = '/my/dir',
    target       = 's3://my-bucket/my-path',
    delay        = 30,
    delete       = true,
    maxProcesses = 2,
    exclude = {
        '/sub/folder/',
    },
    s3 = {
        awscliBinary  = '/usr/local/bin/aws',
        awscliOptions = {'--acl', 'public-read'},
        dryrun        = false
    }
}
