<commit_before>/*
   Copyright The containerd Authors.

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

package local

import (
	"context"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"runtime"
	"time"

	"github.com/containerd/containerd/content"
	"github.com/containerd/containerd/errdefs"
	"github.com/containerd/containerd/log"
	"github.com/opencontainers/go-digest"
)

// writer represents a write transaction against the blob store.
type writer struct {
	s         *store
	fp        *os.File // opened data file
	path      string   // path to writer dir
	ref       string   // ref key
	offset    int64
	total     int64
	digester  digest.Digester
	startedAt time.Time
	updatedAt time.Time
}

func (w *writer) Status() (content.Status, error) {
	return content.Status{
		Ref:       w.ref,
		Offset:    w.offset,
		Total:     w.total,
		StartedAt: w.startedAt,
		UpdatedAt: w.updatedAt,
	}, nil
}

// Digest returns the current digest of the content, up to the current write.
//
// Cannot be called concurrently with `Write`.
func (w *writer) Digest() digest.Digest {
	return w.digester.Digest()
}

// Write p to the transaction.
//
// Note that writes are unbuffered to the backing file. When writing, it is
// recommended to wrap in a bufio.Writer or, preferably, use io.CopyBuffer.
func (w *writer) Write(p []byte) (n int, err error) {
	n, err = w.fp.Write(p)
	w.digester.Hash().Write(p[:n])
	w.offset += int64(len(p))
	w.updatedAt = time.Now()
	return n, err
}

func (w *writer) Commit(ctx context.Context, size int64, expected digest.Digest, opts ...content.Opt) error {
	// Ensure even on error the writer is fully closed
	defer unlock(w.ref)

	var base content.Info
	for _, opt := range opts {
		if err := opt(&base); err != nil {
			return err
		}
	}

	fp := w.fp
	w.fp = nil

	if fp == nil {
		return fmt.Errorf("cannot commit on closed writer: %w", errdefs.ErrFailedPrecondition)
	}

	if err := fp.Sync(); err != nil {
		fp.Close()
		return fmt.Errorf("sync failed: %w", err)
	}

	fi, err := fp.Stat()
	closeErr := fp.Close()
	if err != nil {
		return fmt.Errorf("stat on ingest file failed: %w", err)
	}
	if closeErr != nil {
		return fmt.Errorf("failed to close ingest file: %w", err)
	}

	if size > 0 && size != fi.Size() {
		return fmt.Errorf("unexpected commit size %d, expected %d: %w", fi.Size(), size, errdefs.ErrFailedPrecondition)
	}

	dgst := w.digester.Digest()
	if expected != "" && expected != dgst {
		return fmt.Errorf("unexpected commit digest %s, expected %s: %w", dgst, expected, errdefs.ErrFailedPrecondition)
	}

	var (
		ingest    = filepath.Join(w.path, "data")
		target, _ = w.s.blobPath(dgst) // ignore error because we calculated this dgst
	)

	// make sure parent directories of blob exist
	if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
		return err
	}

	if _, err := os.Stat(target); err == nil {
		// collision with the target file!
		if err := os.RemoveAll(w.path); err != nil {
			log.G(ctx).WithField("ref", w.ref).WithField("path", w.path).Error("failed to remove ingest directory")
		}
		return fmt.Errorf("content %v: %w", dgst, errdefs.ErrAlreadyExists)
	}

	if err := os.Rename(ingest, target); err != nil {
		return err
	}

	// Ingest has now been made available in the content store, attempt to complete
	// setting metadata but errors should only be logged and not returned since
	// the content store cannot be cleanly rolled back.

	commitTime := time.Now()
	if err := os.Chtimes(target, commitTime, commitTime); err != nil {
		log.G(ctx).WithField("digest", dgst).Error("failed to change file time to commit time")
	}

	// clean up!!
	if err := os.RemoveAll(w.path); err != nil {
		log.G(ctx).WithField("ref", w.ref).WithField("path", w.path).Error("failed to remove ingest directory")
	}

	if w.s.ls != nil && base.Labels != nil {
		if err := w.s.ls.Set(dgst, base.Labels); err != nil {
			log.G(ctx).WithField("digest", dgst).Error("failed to set labels")
		}
	}

	// change to readonly, more important for read, but provides _some_
	// protection from this point on. We use the existing perms with a mask
	// only allowing reads honoring the umask on creation.
	//
	// This removes write and exec, only allowing read per the creation umask.
	//
	// NOTE: Windows does not support this operation
	if runtime.GOOS != "windows" {
		if err := os.Chmod(target, (fi.Mode()&os.ModePerm)&^0333); err != nil {
			log.G(ctx).WithField("ref", w.ref).Error("failed to make readonly")
		}
	}

	return nil
}

// Close the writer, flushing any unwritten data and leaving the progress in
// tact.
//
// If one needs to resume the transaction, a new writer can be obtained from
// `Ingester.Writer` using the same key. The write can then be continued
// from it was left off.
//
// To abandon a transaction completely, first call close then `IngestManager.Abort` to
// clean up the associated resources.
func (w *writer) Close() (err error) {
	if w.fp != nil {
		w.fp.Sync()
		err = w.fp.Close()
		writeTimestampFile(filepath.Join(w.path, "updatedat"), w.updatedAt)
		w.fp = nil
		unlock(w.ref)
		return
	}

	return nil
}

func (w *writer) Truncate(size int64) error {
	if size != 0 {
		return errors.New("Truncate: unsupported size")
	}
	w.offset = 0
	w.digester.Hash().Reset()
	if _, err := w.fp.Seek(0, io.SeekStart); err != nil {
		return err
	}
	return w.fp.Truncate(0)
}
<commit_msg>Fix incorrect error wrapped when closing ingest file<commit_after>/*
   Copyright The containerd Authors.

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

package local

import (
	"context"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"runtime"
	"time"

	"github.com/containerd/containerd/content"
	"github.com/containerd/containerd/errdefs"
	"github.com/containerd/containerd/log"
	"github.com/opencontainers/go-digest"
)

// writer represents a write transaction against the blob store.
type writer struct {
	s         *store
	fp        *os.File // opened data file
	path      string   // path to writer dir
	ref       string   // ref key
	offset    int64
	total     int64
	digester  digest.Digester
	startedAt time.Time
	updatedAt time.Time
}

func (w *writer) Status() (content.Status, error) {
	return content.Status{
		Ref:       w.ref,
		Offset:    w.offset,
		Total:     w.total,
		StartedAt: w.startedAt,
		UpdatedAt: w.updatedAt,
	}, nil
}

// Digest returns the current digest of the content, up to the current write.
//
// Cannot be called concurrently with `Write`.
func (w *writer) Digest() digest.Digest {
	return w.digester.Digest()
}

// Write p to the transaction.
//
// Note that writes are unbuffered to the backing file. When writing, it is
// recommended to wrap in a bufio.Writer or, preferably, use io.CopyBuffer.
func (w *writer) Write(p []byte) (n int, err error) {
	n, err = w.fp.Write(p)
	w.digester.Hash().Write(p[:n])
	w.offset += int64(len(p))
	w.updatedAt = time.Now()
	return n, err
}

func (w *writer) Commit(ctx context.Context, size int64, expected digest.Digest, opts ...content.Opt) error {
	// Ensure even on error the writer is fully closed
	defer unlock(w.ref)

	var base content.Info
	for _, opt := range opts {
		if err := opt(&base); err != nil {
			return err
		}
	}

	fp := w.fp
	w.fp = nil

	if fp == nil {
		return fmt.Errorf("cannot commit on closed writer: %w", errdefs.ErrFailedPrecondition)
	}

	if err := fp.Sync(); err != nil {
		fp.Close()
		return fmt.Errorf("sync failed: %w", err)
	}

	fi, err := fp.Stat()
	closeErr := fp.Close()
	if err != nil {
		return fmt.Errorf("stat on ingest file failed: %w", err)
	}
	if closeErr != nil {
		return fmt.Errorf("failed to close ingest file: %w", closeErr)
	}

	if size > 0 && size != fi.Size() {
		return fmt.Errorf("unexpected commit size %d, expected %d: %w", fi.Size(), size, errdefs.ErrFailedPrecondition)
	}

	dgst := w.digester.Digest()
	if expected != "" && expected != dgst {
		return fmt.Errorf("unexpected commit digest %s, expected %s: %w", dgst, expected, errdefs.ErrFailedPrecondition)
	}

	var (
		ingest    = filepath.Join(w.path, "data")
		target, _ = w.s.blobPath(dgst) // ignore error because we calculated this dgst
	)

	// make sure parent directories of blob exist
	if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
		return err
	}

	if _, err := os.Stat(target); err == nil {
		// collision with the target file!
		if err := os.RemoveAll(w.path); err != nil {
			log.G(ctx).WithField("ref", w.ref).WithField("path", w.path).Error("failed to remove ingest directory")
		}
		return fmt.Errorf("content %v: %w", dgst, errdefs.ErrAlreadyExists)
	}

	if err := os.Rename(ingest, target); err != nil {
		return err
	}

	// Ingest has now been made available in the content store, attempt to complete
	// setting metadata but errors should only be logged and not returned since
	// the content store cannot be cleanly rolled back.

	commitTime := time.Now()
	if err := os.Chtimes(target, commitTime, commitTime); err != nil {
		log.G(ctx).WithField("digest", dgst).Error("failed to change file time to commit time")
	}

	// clean up!!
	if err := os.RemoveAll(w.path); err != nil {
		log.G(ctx).WithField("ref", w.ref).WithField("path", w.path).Error("failed to remove ingest directory")
	}

	if w.s.ls != nil && base.Labels != nil {
		if err := w.s.ls.Set(dgst, base.Labels); err != nil {
			log.G(ctx).WithField("digest", dgst).Error("failed to set labels")
		}
	}

	// change to readonly, more important for read, but provides _some_
	// protection from this point on. We use the existing perms with a mask
	// only allowing reads honoring the umask on creation.
	//
	// This removes write and exec, only allowing read per the creation umask.
	//
	// NOTE: Windows does not support this operation
	if runtime.GOOS != "windows" {
		if err := os.Chmod(target, (fi.Mode()&os.ModePerm)&^0333); err != nil {
			log.G(ctx).WithField("ref", w.ref).Error("failed to make readonly")
		}
	}

	return nil
}

// Close the writer, flushing any unwritten data and leaving the progress in
// tact.
//
// If one needs to resume the transaction, a new writer can be obtained from
// `Ingester.Writer` using the same key. The write can then be continued
// from it was left off.
//
// To abandon a transaction completely, first call close then `IngestManager.Abort` to
// clean up the associated resources.
func (w *writer) Close() (err error) {
	if w.fp != nil {
		w.fp.Sync()
		err = w.fp.Close()
		writeTimestampFile(filepath.Join(w.path, "updatedat"), w.updatedAt)
		w.fp = nil
		unlock(w.ref)
		return
	}

	return nil
}

func (w *writer) Truncate(size int64) error {
	if size != 0 {
		return errors.New("Truncate: unsupported size")
	}
	w.offset = 0
	w.digester.Hash().Reset()
	if _, err := w.fp.Seek(0, io.SeekStart); err != nil {
		return err
	}
	return w.fp.Truncate(0)
}
<|endoftext|>


<commit_before>// Copyright © 2017 Igor Bondarenko <ibondare@protonmail.com>
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package context

import (
	"sync"
	"testing"
)

func TestSingleton(t *testing.T) {
	ctx1 := Instance()
	ctx2 := Instance()

	if ctx1 != ctx2 {
		t.Error("Context instance is not a singleton")
	}
}

func TestDisabledSimulateFailure(t *testing.T) {
	expectedHttpCode := 200

	ctx := &Context{
		Mutex: &sync.Mutex{},
		FailureMode: FailureSimulation{
			Enabled: false, FailureCount: 2, SuccessCount: 2, FailureCode: 500},
		HttpCode: expectedHttpCode}

	actualHttpCode := ctx.SimulateFailure()

	if expectedHttpCode != actualHttpCode {
		t.Errorf("Expected HTTP status code %d, got %d", expectedHttpCode, actualHttpCode)
	}

}

func TestSimulateFailure(t *testing.T) {

	tests := []*Context{
		&Context{
			Mutex: &sync.Mutex{},
			FailureMode: FailureSimulation{
				Enabled: true, FailureCount: 5, SuccessCount: 10, FailureCode: 500},
		},
		&Context{
			Mutex: &sync.Mutex{},
			FailureMode: FailureSimulation{
				Enabled: true, FailureCount: 1, SuccessCount: 1, FailureCode: 502},
		},
		&Context{
			Mutex: &sync.Mutex{},
			FailureMode: FailureSimulation{
				Enabled: true, FailureCount: 5, SuccessCount: 0, FailureCode: 500},
		},
		&Context{
			Mutex: &sync.Mutex{},
			FailureMode: FailureSimulation{
				Enabled: true, FailureCount: 0, SuccessCount: 5, FailureCode: 500},
		},
		&Context{
			Mutex: &sync.Mutex{},
			FailureMode: FailureSimulation{
				Enabled: true, FailureCount: 0, SuccessCount: 0, FailureCode: 500},
		},
	}

	for _, ctx := range tests {
		for fCount := 0; fCount < ctx.FailureMode.FailureCount; fCount++ {
			if httpCode := ctx.SimulateFailure(); httpCode != ctx.FailureMode.FailureCode {
				t.Errorf("Expected HTTP status code %d, got %d")
			}
		}
		for sCount := 0; sCount < ctx.FailureMode.SuccessCount; sCount++ {
			if httpCode := ctx.SimulateFailure(); httpCode != ctx.HttpCode {
				t.Errorf("Expected HTTP status code %d, got %d")
			}
		}
	}
}
<commit_msg>Fixed test cases<commit_after>// Copyright © 2017 Igor Bondarenko <ibondare@protonmail.com>
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package context

import (
	"sync"
	"testing"
)

func TestSingleton(t *testing.T) {
	ctx1 := Instance()
	ctx2 := Instance()

	if ctx1 != ctx2 {
		t.Error("Context instance is not a singleton")
	}
}

func TestDisabledSimulateFailure(t *testing.T) {
	expectedHttpCode := 200

	ctx := &Context{
		Mutex: &sync.Mutex{},
		FailureMode: FailureSimulation{
			Enabled: false, FailureCount: 2, SuccessCount: 2, FailureCode: 500},
		HttpCode: expectedHttpCode}

	actualHttpCode := ctx.SimulateFailure()

	if expectedHttpCode != actualHttpCode {
		t.Errorf("Expected HTTP status code %d, got %d", expectedHttpCode, actualHttpCode)
	}

}

func TestSimulateFailure(t *testing.T) {

	tests := []*Context{
		&Context{
			Mutex: &sync.Mutex{},
			FailureMode: FailureSimulation{
				Enabled: true, FailureCount: 5, SuccessCount: 10, FailureCode: 500},
		},
		&Context{
			Mutex: &sync.Mutex{},
			FailureMode: FailureSimulation{
				Enabled: true, FailureCount: 1, SuccessCount: 1, FailureCode: 502},
		},
		&Context{
			Mutex: &sync.Mutex{},
			FailureMode: FailureSimulation{
				Enabled: true, FailureCount: 5, SuccessCount: 0, FailureCode: 500},
		},
		&Context{
			Mutex: &sync.Mutex{},
			FailureMode: FailureSimulation{
				Enabled: true, FailureCount: 0, SuccessCount: 5, FailureCode: 500},
		},
		&Context{
			Mutex: &sync.Mutex{},
			FailureMode: FailureSimulation{
				Enabled: true, FailureCount: 0, SuccessCount: 0, FailureCode: 500},
		},
	}

	for _, ctx := range tests {
		for fCount := 0; fCount < ctx.FailureMode.FailureCount; fCount++ {
			if httpCode := ctx.SimulateFailure(); httpCode != ctx.FailureMode.FailureCode {
				t.Errorf("Expected HTTP status code %d, got %d",
					ctx.FailureMode.FailureCode, httpCode)
			}
		}
		for sCount := 0; sCount < ctx.FailureMode.SuccessCount; sCount++ {
			if httpCode := ctx.SimulateFailure(); httpCode != ctx.HttpCode {
				t.Errorf("Expected HTTP status code %d, got %d",
					ctx.HttpCode, httpCode)
			}
		}
	}
}
<|endoftext|>


<commit_before>package main

import (
	"fmt"
	"github.com/aqatl/mal/dialog"
	"github.com/aqatl/mal/nyaa_scraper"
	"github.com/fatih/color"
	"github.com/jroimartin/gocui"
	"github.com/urfave/cli"
	"math"
	"os/exec"
)

func malNyaaCui(ctx *cli.Context) error {
	_, list, err := loadMAL(ctx)
	if err != nil {
		return err
	}
	cfg := LoadConfig()

	entry := list.GetByID(cfg.SelectedID)
	if entry == nil {
		return fmt.Errorf("no entry found")
	}
	return startNyaaCui(cfg, entry.Title)
}

func alNyaaCui(ctx *cli.Context) error {
	al, err := loadAniList()
	if err != nil {
		return err
	}
	cfg := LoadConfig()

	entry := al.GetMediaListById(cfg.ALSelectedID)
	if entry == nil {
		return fmt.Errorf("no entry found")
	}
	return startNyaaCui(cfg, entry.Title.Romaji)
}

func startNyaaCui(cfg *Config, searchTerm string) error {
	gui, err := gocui.NewGui(gocui.Output256)
	defer gui.Close()
	if err != nil {
		return fmt.Errorf("gocui error: %v", err)
	}
	nc := &nyaaCui{
		Cfg: cfg,

		SearchTerm: searchTerm,
		Category:   nyaa_scraper.AnimeEnglishTranslated,
		Filter:     nyaa_scraper.NoFilter,
	}
	gui.SetManager(nc)
	nc.setGuiKeyBindings(gui)

	gui.Cursor = false
	gui.Mouse = false
	gui.Highlight = true
	gui.SelFgColor = gocui.ColorGreen

	gui.Update(func(gui *gocui.Gui) error {
		nc.Reload(gui)
		return nil
	})

	if err = gui.MainLoop(); err != nil && err != gocui.ErrQuit {
		return err
	}
	return nil
}

const (
	ncInfoView      = "ncInfoView"
	ncResultsView   = "ncResultsView "
	ncShortcutsView = "ncShortcutsView"
)

type nyaaCui struct {
	Cfg *Config

	SearchTerm string
	Category   nyaa_scraper.NyaaCategory
	Filter     nyaa_scraper.NyaaFilter

	Results     []nyaa_scraper.NyaaEntry
	MaxResults  int
	MaxPages    int
	LoadedPages int

	ResultsView *gocui.View
}

func (nc *nyaaCui) Layout(gui *gocui.Gui) error {
	w, h := gui.Size()
	if v, err := gui.SetView(ncInfoView, 0, 0, w-1, 2); err != nil {
		if err != gocui.ErrUnknownView {
			return err
		}

		v.Title = "Info"
		v.Editable = false

		fmt.Fprintf(v, "[%s]: displaying %d out of %d results",
			nc.SearchTerm, len(nc.Results), nc.MaxResults)
	}

	if v, err := gui.SetView(ncResultsView, 0, 3, w-1, h-4); err != nil {
		if err != gocui.ErrUnknownView {
			return err
		}

		v.Title = "Search results"
		v.SelBgColor = gocui.ColorGreen
		v.SelBgColor = gocui.ColorGreen
		v.SelFgColor = gocui.ColorBlack
		v.Highlight = true
		v.Editable = true
		v.Editor = gocui.EditorFunc(nc.Editor(gui))

		gui.SetCurrentView(ncResultsView)
		nc.ResultsView = v

		//TODO Better/clearer results printing
		for _, result := range nc.Results {
			fmt.Fprintf(v, "%s %s %v %d %d %d\n",
				result.Title,
				result.Size,
				result.DateAdded,
				result.Seeders,
				result.Leechers,
				result.CompletedDownloads,
			)
		}
	}

	if v, err := gui.SetView(ncShortcutsView, 0, h-3, w-1, h-1); err != nil {
		if err != gocui.ErrUnknownView {
			return err
		}

		v.Title = "Shortcuts"
		v.Editable = false

		c := color.New(color.FgCyan).SprintFunc()
		fmt.Fprintln(v, c("d"), "download", c("l"), "load next page",
			c("c"), "category", c("f"), "filters")
	}

	return nil
}

func (nc *nyaaCui) Editor(gui *gocui.Gui) func(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
	//TODO it's too big
	return func(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
		switch {
		case key == gocui.KeyArrowDown || ch == 'j':
			_, oy := v.Origin()
			_, y := v.Cursor()
			y += oy
			if y < len(nc.Results)-1 {
				v.MoveCursor(0, 1, false)
			}
		case key == gocui.KeyArrowUp || ch == 'k':
			v.MoveCursor(0, -1, false)
		case ch == 'd':
			_, y := v.Cursor()
			_, oy := v.Origin()
			y += oy
			if y >= len(nc.Results) {
				return
			}

			link := ""
			if entry := nc.Results[y]; entry.MagnetLink != "" {
				link = entry.MagnetLink
			} else if entry.TorrentLink != "" {
				link = entry.TorrentLink
			} else {
				dialog.JustShowOkDialog(gui, "Error", "No link found")
				return
			}

			if err := nc.Download(link); err != nil {
				gui.Update(func(gui *gocui.Gui) error {
					return err
				})
			}
		case ch == 'l':
			if nc.LoadedPages >= nc.MaxPages {
				return
			}
			nc.LoadedPages++
			go func() {
				resultPage, _ := nyaa_scraper.SearchSpecificPage(
					nc.SearchTerm,
					nyaa_scraper.AnimeEnglishTranslated,
					nyaa_scraper.NoFilter,
					nc.LoadedPages-1,
				)
				nc.Results = append(nc.Results, resultPage.Results...)
				gui.Update(func(gui *gocui.Gui) error {
					_, oy := nc.ResultsView.Origin()
					_, y := nc.ResultsView.Cursor()

					gui.DeleteView(ncInfoView)
					gui.DeleteView(ncResultsView)

					nc.Layout(gui)
					nc.ResultsView.SetOrigin(0, oy)
					nc.ResultsView.SetCursor(0, y)

					return nil
				})
			}()
		case ch == 'c':
			categories := make([]fmt.Stringer, len(nyaa_scraper.Categories))
			for i := range categories {
				categories[i] = nyaa_scraper.Categories[i]
			}
			selIdxChan, cleanUp, err := dialog.ListSelect(gui, "Select category", categories)
			if err != nil {
				gocuiReturnError(gui, err)
			}
			go func() {
				idx, ok := <-selIdxChan
				gui.Update(cleanUp)
				if ok {
					nc.Category = nyaa_scraper.Categories[idx]
					nc.Reload(gui)
				}
			}()
		case ch == 'f':
			filters := make([]fmt.Stringer, len(nyaa_scraper.Filters))
			for i := range filters {
				filters[i] = nyaa_scraper.Filters[i]
			}
			selIdxChan, cleanUp, err := dialog.ListSelect(gui, "Select filter", filters)
			if err != nil {
				gocuiReturnError(gui, err)
			}
			go func() {
				idx, ok := <-selIdxChan
				gui.Update(cleanUp)
				if ok {
					nc.Filter = nyaa_scraper.Filters[idx]
					nc.Reload(gui)
				}
			}()
		}
	}
}

func (nc *nyaaCui) Reload(gui *gocui.Gui) {
	var resultPage nyaa_scraper.NyaaResultPage
	var searchErr error
	f := func() {
		resultPage, searchErr = nyaa_scraper.Search(nc.SearchTerm, nc.Category, nc.Filter)
	}
	jobDone, err := dialog.StuffLoader(dialog.FitMessage(gui, "Loading "+nc.SearchTerm), f)
	if err != nil {
		gocuiReturnError(gui, err)
	}
	go func() {
		ok := <-jobDone
		if searchErr != nil {
			dialog.JustShowOkDialog(gui, "Error", searchErr.Error())
			return
		}
		if ok {
			nc.Results = resultPage.Results
			nc.MaxResults = resultPage.DisplayedOutOf
			nc.MaxPages = int(math.Ceil(float64(resultPage.DisplayedOutOf) /
				float64(resultPage.DisplayedTo-resultPage.DisplayedFrom+1)))
			nc.LoadedPages = 1
		}

		gui.Update(func(gui *gocui.Gui) error {
			gui.DeleteView(ncResultsView)
			gui.DeleteView(ncInfoView)
			return nil
		})
	}()
}

func (nc *nyaaCui) Download(link string) error {
	link = "\"" + link + "\""
	cmd := exec.Command(nc.Cfg.TorrentClientPath, nc.Cfg.TorrentClientArgs, link)
	cmd.Args = cmd.Args[1:] //Why they include app name in the arguments???
	return cmd.Start()
}

func (nc *nyaaCui) setGuiKeyBindings(gui *gocui.Gui) {
	gui.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quitGocui)
}

func quitGocui(gui *gocui.Gui, view *gocui.View) error {
	return gocui.ErrQuit
}

func gocuiReturnError(gui *gocui.Gui, err error) {
	gui.Update(func(gui *gocui.Gui) error {
		return err
	})
}
<commit_msg>Fixed nyaa cui bug<commit_after>package main

import (
	"fmt"
	"github.com/aqatl/mal/dialog"
	"github.com/aqatl/mal/nyaa_scraper"
	"github.com/fatih/color"
	"github.com/jroimartin/gocui"
	"github.com/urfave/cli"
	"math"
	"os/exec"
)

func malNyaaCui(ctx *cli.Context) error {
	_, list, err := loadMAL(ctx)
	if err != nil {
		return err
	}
	cfg := LoadConfig()

	entry := list.GetByID(cfg.SelectedID)
	if entry == nil {
		return fmt.Errorf("no entry found")
	}
	return startNyaaCui(cfg, entry.Title)
}

func alNyaaCui(ctx *cli.Context) error {
	al, err := loadAniList()
	if err != nil {
		return err
	}
	cfg := LoadConfig()

	entry := al.GetMediaListById(cfg.ALSelectedID)
	if entry == nil {
		return fmt.Errorf("no entry found")
	}
	return startNyaaCui(cfg, entry.Title.Romaji)
}

func startNyaaCui(cfg *Config, searchTerm string) error {
	gui, err := gocui.NewGui(gocui.Output256)
	defer gui.Close()
	if err != nil {
		return fmt.Errorf("gocui error: %v", err)
	}
	nc := &nyaaCui{
		Cfg: cfg,

		SearchTerm: searchTerm,
		Category:   nyaa_scraper.AnimeEnglishTranslated,
		Filter:     nyaa_scraper.NoFilter,
	}
	gui.SetManager(nc)
	nc.setGuiKeyBindings(gui)

	gui.Cursor = false
	gui.Mouse = false
	gui.Highlight = true
	gui.SelFgColor = gocui.ColorGreen

	gui.Update(func(gui *gocui.Gui) error {
		nc.Reload(gui)
		return nil
	})

	if err = gui.MainLoop(); err != nil && err != gocui.ErrQuit {
		return err
	}
	return nil
}

const (
	ncInfoView      = "ncInfoView"
	ncResultsView   = "ncResultsView "
	ncShortcutsView = "ncShortcutsView"
)

type nyaaCui struct {
	Cfg *Config

	SearchTerm string
	Category   nyaa_scraper.NyaaCategory
	Filter     nyaa_scraper.NyaaFilter

	Results     []nyaa_scraper.NyaaEntry
	MaxResults  int
	MaxPages    int
	LoadedPages int

	ResultsView *gocui.View
}

func (nc *nyaaCui) Layout(gui *gocui.Gui) error {
	w, h := gui.Size()
	if v, err := gui.SetView(ncInfoView, 0, 0, w-1, 2); err != nil {
		if err != gocui.ErrUnknownView {
			return err
		}

		v.Title = "Info"
		v.Editable = false

		fmt.Fprintf(v, "[%s]: displaying %d out of %d results",
			nc.SearchTerm, len(nc.Results), nc.MaxResults)
	}

	if v, err := gui.SetView(ncResultsView, 0, 3, w-1, h-4); err != nil {
		if err != gocui.ErrUnknownView {
			return err
		}

		v.Title = "Search results"
		v.SelBgColor = gocui.ColorGreen
		v.SelBgColor = gocui.ColorGreen
		v.SelFgColor = gocui.ColorBlack
		v.Highlight = true
		v.Editable = true
		v.Editor = gocui.EditorFunc(nc.Editor(gui))

		gui.SetCurrentView(ncResultsView)
		nc.ResultsView = v

		//TODO Better/clearer results printing
		for _, result := range nc.Results {
			fmt.Fprintf(v, "%s %s %v %d %d %d\n",
				result.Title,
				result.Size,
				result.DateAdded,
				result.Seeders,
				result.Leechers,
				result.CompletedDownloads,
			)
		}
	}

	if v, err := gui.SetView(ncShortcutsView, 0, h-3, w-1, h-1); err != nil {
		if err != gocui.ErrUnknownView {
			return err
		}

		v.Title = "Shortcuts"
		v.Editable = false

		c := color.New(color.FgCyan).SprintFunc()
		fmt.Fprintln(v, c("d"), "download", c("l"), "load next page",
			c("c"), "category", c("f"), "filters")
	}

	return nil
}

func (nc *nyaaCui) Editor(gui *gocui.Gui) func(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
	//TODO it's too big
	return func(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
		switch {
		case key == gocui.KeyArrowDown || ch == 'j':
			_, oy := v.Origin()
			_, y := v.Cursor()
			y += oy
			if y < len(nc.Results)-1 {
				v.MoveCursor(0, 1, false)
			}
		case key == gocui.KeyArrowUp || ch == 'k':
			v.MoveCursor(0, -1, false)
		case ch == 'd':
			_, y := v.Cursor()
			_, oy := v.Origin()
			y += oy
			if y >= len(nc.Results) {
				return
			}

			link := ""
			if entry := nc.Results[y]; entry.MagnetLink != "" {
				link = entry.MagnetLink
			} else if entry.TorrentLink != "" {
				link = entry.TorrentLink
			} else {
				dialog.JustShowOkDialog(gui, "Error", "No link found")
				return
			}

			if err := nc.Download(link); err != nil {
				gui.Update(func(gui *gocui.Gui) error {
					return err
				})
			}
		case ch == 'l':
			if nc.LoadedPages >= nc.MaxPages {
				return
			}
			nc.LoadedPages++
			go func() {
				resultPage, _ := nyaa_scraper.SearchSpecificPage(
					nc.SearchTerm,
					nc.Category,
					nc.Filter,
					nc.LoadedPages,
				)
				nc.Results = append(nc.Results, resultPage.Results...)
				gui.Update(func(gui *gocui.Gui) error {
					_, oy := nc.ResultsView.Origin()
					_, y := nc.ResultsView.Cursor()

					gui.DeleteView(ncInfoView)
					gui.DeleteView(ncResultsView)

					nc.Layout(gui)
					nc.ResultsView.SetOrigin(0, oy)
					nc.ResultsView.SetCursor(0, y)

					return nil
				})
			}()
		case ch == 'c':
			categories := make([]fmt.Stringer, len(nyaa_scraper.Categories))
			for i := range categories {
				categories[i] = nyaa_scraper.Categories[i]
			}
			selIdxChan, cleanUp, err := dialog.ListSelect(gui, "Select category", categories)
			if err != nil {
				gocuiReturnError(gui, err)
			}
			go func() {
				idx, ok := <-selIdxChan
				gui.Update(cleanUp)
				if ok {
					nc.Category = nyaa_scraper.Categories[idx]
					nc.Reload(gui)
				}
			}()
		case ch == 'f':
			filters := make([]fmt.Stringer, len(nyaa_scraper.Filters))
			for i := range filters {
				filters[i] = nyaa_scraper.Filters[i]
			}
			selIdxChan, cleanUp, err := dialog.ListSelect(gui, "Select filter", filters)
			if err != nil {
				gocuiReturnError(gui, err)
			}
			go func() {
				idx, ok := <-selIdxChan
				gui.Update(cleanUp)
				if ok {
					nc.Filter = nyaa_scraper.Filters[idx]
					nc.Reload(gui)
				}
			}()
		}
	}
}

func (nc *nyaaCui) Reload(gui *gocui.Gui) {
	var resultPage nyaa_scraper.NyaaResultPage
	var searchErr error
	f := func() {
		resultPage, searchErr = nyaa_scraper.Search(nc.SearchTerm, nc.Category, nc.Filter)
	}
	jobDone, err := dialog.StuffLoader(dialog.FitMessage(gui, "Loading "+nc.SearchTerm), f)
	if err != nil {
		gocuiReturnError(gui, err)
	}
	go func() {
		ok := <-jobDone
		if searchErr != nil {
			dialog.JustShowOkDialog(gui, "Error", searchErr.Error())
			return
		}
		if ok {
			nc.Results = resultPage.Results
			nc.MaxResults = resultPage.DisplayedOutOf
			nc.MaxPages = int(math.Ceil(float64(resultPage.DisplayedOutOf) /
				float64(resultPage.DisplayedTo-resultPage.DisplayedFrom+1)))
			nc.LoadedPages = 1
		}

		gui.Update(func(gui *gocui.Gui) error {
			gui.DeleteView(ncResultsView)
			gui.DeleteView(ncInfoView)
			return nil
		})
	}()
}

func (nc *nyaaCui) Download(link string) error {
	link = "\"" + link + "\""
	cmd := exec.Command(nc.Cfg.TorrentClientPath, nc.Cfg.TorrentClientArgs, link)
	cmd.Args = cmd.Args[1:] //Why they include app name in the arguments???
	return cmd.Start()
}

func (nc *nyaaCui) setGuiKeyBindings(gui *gocui.Gui) {
	gui.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quitGocui)
}

func quitGocui(gui *gocui.Gui, view *gocui.View) error {
	return gocui.ErrQuit
}

func gocuiReturnError(gui *gocui.Gui, err error) {
	gui.Update(func(gui *gocui.Gui) error {
		return err
	})
}
<|endoftext|>


<commit_before>// Obdi - a REST interface and GUI for deploying software
// Copyright (C) 2014  Mark Clarkson
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

package main

import (
	"fmt"
	"github.com/mclarkson/obdi/external/jinzhu/gorm"
	"html/template"
	"net/http"
	"os"
	"path"
	"sync"
	"time"
	//"strconv"
)

var VERSION string

type Api struct {
	db       *gorm.DB
	port     int64
	apimutex *sync.Mutex
	compile  *sync.Mutex
}

type ApiError struct {
	details string
}

// SetDB: Allows to set the gorm.DB
//
func (api *Api) SetDB(db *gorm.DB) {
	api.db = db
}

// Port: Return a port to connect to for RPC and increment it for the
// next connection
func (api *Api) Port() int64 {
	apimutex.Lock()
	portnum := api.port
	api.port += 1
	apimutex.Unlock()
	return portnum
}

func (api *Api) SetPort(portnum int64) {
	apimutex.Lock()
	api.port = portnum
	apimutex.Unlock()
}

func (api *Api) DecrementPort() {
	apimutex.Lock()
	//startport,_ := strconv.ParseInt(config.GoPluginPortStart,10,64)
	if api.port != config.GoPluginPortStart {
		api.port -= 1
	}
	apimutex.Unlock()
}

// TouchSession: Updates the UpdatedAt field
//
// Takes the GUID is a parameter.
//
func (api *Api) TouchSession(guid string) {
	session := Session{}
	mutex.Lock()
	api.db.Where("guid = ?", guid).First(&session)
	session.UpdatedAt = time.Now()
	api.db.Save(&session)
	mutex.Unlock()
}

// LogActivity: Write a message to the activity log
//
func (api *Api) LogActivity(sesId int64, message string) {
	activity := Activity{
		Session_id: sesId,
		Message:    message,
	}
	mutex.Lock()
	api.db.Save(&activity)
	mutex.Unlock()
}

func (e ApiError) Error() string {
	return fmt.Sprintf("%s", e.details)
}

func (api *Api) CheckLoginNoExpiry(login, guid string) (Session, error) {

	user := User{}
	session := Session{}

	// select * from users where login = login
	mutex.Lock()
	if err := api.db.Where("login = ?", login).First(&user).Error; err != nil {
		mutex.Unlock()
		return session, ApiError{"Invalid credentials."}
	}
	// select * from sessions where user_id = user.userid
	if err := api.db.Model(&user).Related(&session).Error; err != nil {
		mutex.Unlock()
		return session, ApiError{"Not logged in."}
	}
	mutex.Unlock()

	// Check GUID
	if session.Guid != guid {
		return session, ApiError{"Invalid GUID."}
	}

	return session, nil
}

func (api *Api) CheckLogin(login, guid string) (Session, error) {

	user := User{}
	session := Session{}

	// select * from users where login = login
	mutex.Lock()
	if err := api.db.Where("login = ?", login).First(&user).Error; err != nil {
		mutex.Unlock()
		return session, ApiError{"Invalid credentials."}
	}
	mutex.Unlock()

	// select * from sessions where user_id = user.userid
	mutex.Lock()
	if err := api.db.Model(&user).Related(&session).Error; err != nil {
		mutex.Unlock()
		return session, ApiError{"Not logged in."}
	}
	mutex.Unlock()

	// Check GUID
	if session.Guid != guid {
		return session, ApiError{"Invalid GUID."}
	}

	// Check session age
	delta := time.Now().Sub(session.UpdatedAt)
	if delta.Minutes() > float64(config.SessionTimeout) {
		return session, ApiError{"Session expired."}
	}

	return session, nil
}

/*
 * The only purpose of serveRunTemplate is to add items to
 * the <head> block. Specifically to add AngularJS controller
 * files to support plugins.
 */
func (api *Api) serveRunTemplate(w http.ResponseWriter, r *http.Request) {

	/* Refer to:
	   http://www.alexedwards.net/blog/serving-static-sites-with-go
	   and
	   http://www.alexedwards.net/blog/a-recap-of-request-handling
	*/

	defaultScripts := []string{}

	// Split the default scripts for admin or run(user)
	// It's alot of unused scripts otherwise.
	if match, _ := path.Match("/manager/admin", r.URL.Path); match == true {
		defaultScripts = []string{
			`js/controllers/login.js`,
			`js/controllers/admin.js`,
			`js/controllers/users.js`,
			`js/controllers/dcs.js`,
			`js/controllers/envs.js`,
			`js/controllers/dccaps.js`,
			`js/controllers/envcaps.js`,
			`js/controllers/scripts.js`,
			`js/controllers/plugins.js`,
		}
	} else {
		// It's /manager/run
		defaultScripts = []string{
			`js/controllers/login.js`,
			`js/controllers/run.js`,
			`js/controllers/sidebar.js`,
		}
	}

	type IndexPageVars struct {
		Items   []string
		Version string
	}

	fp := path.Join(config.StaticContent + "/templates/main-index.html")

	logit(fp)
	// Return a 404 if the template doesn't exist
	info, err := os.Stat(fp)
	if err != nil {
		if os.IsNotExist(err) {
			http.NotFound(w, r)
			return
		}
	}

	// Return a 404 if the request is for a directory
	if info.IsDir() {
		http.NotFound(w, r)
		return
	}

	// AngularJS uses {{ }} so we'll use [(
	// The following line creates an unnamed template and sets the
	// Delims for it. Parse files creates a further template named
	// as the file name - this name is used to execute the template
	// and it inherits the Delims. There's no other way to set the
	// Delims!
	tmpl, err := template.New("").Delims(`[(`, `)]`).ParseFiles(fp)
	if err != nil {
		// Log the detailed error
		logit(err.Error())
		// Return a generic "Internal Server Error" message
		http.Error(w, http.StatusText(500), 500)
		return
	}

	// Search Files table for controllers that need adding to the
	// HEAD section
	files := []File{}
	mutex.Lock()
	api.db.Order("name").Find(&files, `url != "" and type == 1`)
	mutex.Unlock()

	//fmt.Printf( "%#v\n", files )
	scripts := []string{}
	for i := range files {
		scripts = append(scripts, "plugins/"+files[i].Url)
	}
	defaultScripts = append(defaultScripts, scripts...)

	data := &IndexPageVars{defaultScripts, VERSION}
	if err := tmpl.ExecuteTemplate(w, path.Base(fp), data); err != nil {
		logit(err.Error())
		http.Error(w, http.StatusText(500), 500)
	}
}

func NewApi(db *Database) Api {
	api := Api{}
	api.SetDB(&db.dB)
	//startport,_ := strconv.ParseInt(config.GoPluginPortStart,10,64)
	api.SetPort(config.GoPluginPortStart)
	api.compile = &sync.Mutex{}

	return api
}
<commit_msg>Fixed compile error<commit_after>// Obdi - a REST interface and GUI for deploying software
// Copyright (C) 2014  Mark Clarkson
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

package main

import (
	"fmt"
	"github.com/mclarkson/obdi/external/jinzhu/gorm"
	"html/template"
	"net/http"
	"os"
	"path"
	"sync"
)

var VERSION string

type Api struct {
	db       *gorm.DB
	port     int64
	apimutex *sync.Mutex
	compile  *sync.Mutex
}

type ApiError struct {
	details string
}

// SetDB: Allows to set the gorm.DB
//
func (api *Api) SetDB(db *gorm.DB) {
	api.db = db
}

// Port: Return a port to connect to for RPC and increment it for the
// next connection
func (api *Api) Port() int64 {
	apimutex.Lock()
	portnum := api.port
	api.port += 1
	apimutex.Unlock()
	return portnum
}

func (api *Api) SetPort(portnum int64) {
	apimutex.Lock()
	api.port = portnum
	apimutex.Unlock()
}

func (api *Api) DecrementPort() {
	apimutex.Lock()
	//startport,_ := strconv.ParseInt(config.GoPluginPortStart,10,64)
	if api.port != config.GoPluginPortStart {
		api.port -= 1
	}
	apimutex.Unlock()
}

// TouchSession: Updates the UpdatedAt field
//
// Takes the GUID is a parameter.
//
func (api *Api) TouchSession(guid string) {
	session := Session{}
	mutex.Lock()
	api.db.Where("guid = ?", guid).First(&session)
	session.UpdatedAt = time.Now()
	api.db.Save(&session)
	mutex.Unlock()
}

// LogActivity: Write a message to the activity log
//
func (api *Api) LogActivity(sesId int64, message string) {
	activity := Activity{
		Session_id: sesId,
		Message:    message,
	}
	mutex.Lock()
	api.db.Save(&activity)
	mutex.Unlock()
}

func (e ApiError) Error() string {
	return fmt.Sprintf("%s", e.details)
}

func (api *Api) CheckLoginNoExpiry(login, guid string) (Session, error) {

	user := User{}
	session := Session{}

	// select * from users where login = login
	mutex.Lock()
	if err := api.db.Where("login = ?", login).First(&user).Error; err != nil {
		mutex.Unlock()
		return session, ApiError{"Invalid credentials."}
	}
	// select * from sessions where user_id = user.userid
	if err := api.db.Model(&user).Related(&session).Error; err != nil {
		mutex.Unlock()
		return session, ApiError{"Not logged in."}
	}
	mutex.Unlock()

	// Check GUID
	if session.Guid != guid {
		return session, ApiError{"Invalid GUID."}
	}

	return session, nil
}

func (api *Api) CheckLogin(login, guid string) (Session, error) {

	user := User{}
	session := Session{}

	// select * from users where login = login
	mutex.Lock()
	if err := api.db.Where("login = ?", login).First(&user).Error; err != nil {
		mutex.Unlock()
		return session, ApiError{"Invalid credentials."}
	}
	mutex.Unlock()

	// select * from sessions where user_id = user.userid
	mutex.Lock()
	if err := api.db.Model(&user).Related(&session).Error; err != nil {
		mutex.Unlock()
		return session, ApiError{"Not logged in."}
	}
	mutex.Unlock()

	// Check GUID
	if session.Guid != guid {
		return session, ApiError{"Invalid GUID."}
	}

	// Check session age
	delta := time.Now().Sub(session.UpdatedAt)
	if delta.Minutes() > float64(config.SessionTimeout) {
		return session, ApiError{"Session expired."}
	}

	return session, nil
}

/*
 * The only purpose of serveRunTemplate is to add items to
 * the <head> block. Specifically to add AngularJS controller
 * files to support plugins.
 */
func (api *Api) serveRunTemplate(w http.ResponseWriter, r *http.Request) {

	/* Refer to:
	   http://www.alexedwards.net/blog/serving-static-sites-with-go
	   and
	   http://www.alexedwards.net/blog/a-recap-of-request-handling
	*/

	defaultScripts := []string{}

	// Split the default scripts for admin or run(user)
	// It's alot of unused scripts otherwise.
	if match, _ := path.Match("/manager/admin", r.URL.Path); match == true {
		defaultScripts = []string{
			`js/controllers/login.js`,
			`js/controllers/admin.js`,
			`js/controllers/users.js`,
			`js/controllers/dcs.js`,
			`js/controllers/envs.js`,
			`js/controllers/dccaps.js`,
			`js/controllers/envcaps.js`,
			`js/controllers/scripts.js`,
			`js/controllers/plugins.js`,
		}
	} else {
		// It's /manager/run
		defaultScripts = []string{
			`js/controllers/login.js`,
			`js/controllers/run.js`,
			`js/controllers/sidebar.js`,
		}
	}

	type IndexPageVars struct {
		Items   []string
		Version string
	}

	fp := path.Join(config.StaticContent + "/templates/main-index.html")

	logit(fp)
	// Return a 404 if the template doesn't exist
	info, err := os.Stat(fp)
	if err != nil {
		if os.IsNotExist(err) {
			http.NotFound(w, r)
			return
		}
	}

	// Return a 404 if the request is for a directory
	if info.IsDir() {
		http.NotFound(w, r)
		return
	}

	// AngularJS uses {{ }} so we'll use [(
	// The following line creates an unnamed template and sets the
	// Delims for it. Parse files creates a further template named
	// as the file name - this name is used to execute the template
	// and it inherits the Delims. There's no other way to set the
	// Delims!
	tmpl, err := template.New("").Delims(`[(`, `)]`).ParseFiles(fp)
	if err != nil {
		// Log the detailed error
		logit(err.Error())
		// Return a generic "Internal Server Error" message
		http.Error(w, http.StatusText(500), 500)
		return
	}

	// Search Files table for controllers that need adding to the
	// HEAD section
	files := []File{}
	mutex.Lock()
	api.db.Order("name").Find(&files, `url != "" and type == 1`)
	mutex.Unlock()

	//fmt.Printf( "%#v\n", files )
	scripts := []string{}
	for i := range files {
		scripts = append(scripts, "plugins/"+files[i].Url)
	}
	defaultScripts = append(defaultScripts, scripts...)

	data := &IndexPageVars{defaultScripts, VERSION}
	if err := tmpl.ExecuteTemplate(w, path.Base(fp), data); err != nil {
		logit(err.Error())
		http.Error(w, http.StatusText(500), 500)
	}
}

func NewApi(db *Database) Api {
	api := Api{}
	api.SetDB(&db.dB)
	//startport,_ := strconv.ParseInt(config.GoPluginPortStart,10,64)
	api.SetPort(config.GoPluginPortStart)
	api.compile = &sync.Mutex{}

	return api
}
<|endoftext|>


<commit_before>package controllers

import (
	"encoding/json"
	"net/http"

	"github.com/astaxie/beego"
	"github.com/dockercn/wharf/models"
	"github.com/dockercn/wharf/utils"
)

type OrganizationWebV1Controller struct {
	beego.Controller
}

func (this *OrganizationWebV1Controller) URLMapping() {
	this.Mapping("PostOrganization", this.PostOrganization)
	this.Mapping("PutOrganization", this.PutOrganization)
	this.Mapping("GetOrganizations", this.GetOrganizations)
	this.Mapping("GetOrganizationDetail", this.GetOrganizationDetail)
}

func (this *OrganizationWebV1Controller) Prepare() {
	beego.Debug("[Header] ")
	beego.Debug(this.Ctx.Request.Header)

	this.Ctx.Output.Context.ResponseWriter.Header().Set("Content-Type", "application/json;charset=UTF-8")
}

func (this *OrganizationWebV1Controller) PostOrganization() {

	user, exist := this.Ctx.Input.CruSession.Get("user").(models.User)

	if exist != true {

		beego.Error("[WEB API] Load session failure")

		result := map[string]string{"message": "Session load failure", "url": "/auth"}
		this.Data["json"] = &result

		this.Ctx.Output.Context.Output.SetStatus(http.StatusBadRequest)
		this.ServeJson()
		this.StopRun()

	}

	var org models.Organization

	if err := json.Unmarshal(this.Ctx.Input.CopyBody(), &org); err != nil {

		beego.Error("[WEB API] Unmarshal organization data error:", err.Error())

		result := map[string]string{"message": err.Error()}
		this.Data["json"] = result

		this.Ctx.Output.Context.Output.SetStatus(http.StatusBadRequest)
		this.ServeJson()
		this.StopRun()
	}

	beego.Debug("[WEB API] organization create: %s", string(this.Ctx.Input.CopyBody()))

	org.UUID = utils.GeneralToken(org.Organization)

	org.Username = user.Username

	if err := org.Save(); err != nil {
		beego.Error("[WEB API] Organization save error:", err.Error())

		result := map[string]string{"message": "Organization save error."}
		this.Data["json"] = result

		this.Ctx.Output.Context.Output.SetStatus(http.StatusBadRequest)
		this.ServeJson()
		this.StopRun()
	}

	user.Organizations = append(user.Organizations, org.UUID)

	if err := user.Save(); err != nil {
		beego.Error("[WEB API] User save error:", err.Error())

		result := map[string]string{"message": "User save error."}
		this.Data["json"] = result

		this.Ctx.Output.Context.Output.SetStatus(http.StatusBadRequest)
		this.ServeJson()
		this.StopRun()
	}

	user.Get(user.Username, user.Password)
	this.Ctx.Input.CruSession.Set("user", user)

	result := map[string]string{"message": "Create organization successfully."}
	this.Data["json"] = result

	this.Ctx.Output.Context.Output.SetStatus(http.StatusOK)
	this.ServeJson()
	this.StopRun()
}

func (this *OrganizationWebV1Controller) PutOrganization() {

	if _, exist := this.Ctx.Input.CruSession.Get("user").(models.User); exist != true {

		beego.Error("[WEB API] Load session failure")

		result := map[string]string{"message": "Session load failure", "url": "/auth"}
		this.Data["json"] = &result

		this.Ctx.Output.Context.Output.SetStatus(http.StatusBadRequest)
		this.ServeJson()

	} else {

		var org models.Organization

		if err := json.Unmarshal(this.Ctx.Input.CopyBody(), &org); err != nil {
			beego.Error("[WEB API] Unmarshal organization data error:", err.Error())

			result := map[string]string{"message": err.Error()}
			this.Data["json"] = result

			this.Ctx.Output.Context.Output.SetStatus(http.StatusBadRequest)
			this.ServeJson()
		}

		beego.Debug("[WEB API] organization update: %s", string(this.Ctx.Input.CopyBody()))

		if err := org.Save(); err != nil {
			beego.Error("[WEB API] Organization save error:", err.Error())

			result := map[string]string{"message": "Organization save error."}
			this.Data["json"] = result

			this.Ctx.Output.Context.Output.SetStatus(http.StatusBadRequest)
			this.ServeJson()
		}

		result := map[string]string{"message": "Update organization successfully."}
		this.Data["json"] = result

		this.Ctx.Output.Context.Output.SetStatus(http.StatusOK)
		this.ServeJson()
	}
}

func (this *OrganizationWebV1Controller) GetOrganizations() {
	if user, exist := this.Ctx.Input.CruSession.Get("user").(models.User); exist != true {

		beego.Error("[WEB API] Load session failure")

		result := map[string]string{"message": "Session load failure", "url": "/auth"}
		this.Data["json"] = &result

		this.Ctx.Output.Context.Output.SetStatus(http.StatusBadRequest)
		this.ServeJson()

	} else {

		organizations := make([]models.Organization, len(user.Organizations))

		for i, UUID := range user.Organizations {
			if err := organizations[i].Get(UUID); err != nil {
				beego.Error("[WEB API] Get organizations error:", err.Error())

				result := map[string]string{"message": "Get organizations error."}
				this.Data["json"] = result

				this.Ctx.Output.Context.Output.SetStatus(http.StatusBadRequest)
				this.ServeJson()
			}
		}

		this.Data["json"] = organizations

		this.Ctx.Output.Context.Output.SetStatus(http.StatusOK)
		this.ServeJson()
	}
}

func (this *OrganizationWebV1Controller) GetOrganizationDetail() {
	if _, exist := this.Ctx.Input.CruSession.Get("user").(models.User); exist != true {

		beego.Error("[WEB API] Load session failure")

		result := map[string]string{"message": "Session load failure", "url": "/auth"}
		this.Data["json"] = &result

		this.Ctx.Output.Context.Output.SetStatus(http.StatusBadRequest)
		this.ServeJson()

	} else {
		organization := new(models.Organization)

		if _, _, err := organization.Has(this.Ctx.Input.Param(":org")); err != nil {
			beego.Error("[WEB API] Get organizations error:", err.Error())

			result := map[string]string{"message": "Get organizations error."}
			this.Data["json"] = result

			this.Ctx.Output.Context.Output.SetStatus(http.StatusBadRequest)
			this.ServeJson()
		}

		this.Data["json"] = organization

		this.Ctx.Output.Context.Output.SetStatus(http.StatusOK)
		this.ServeJson()
	}
}
<commit_msg>增加获取组织下所有仓库的方法<commit_after>package controllers

import (
	"encoding/json"
	"net/http"

	"github.com/astaxie/beego"
	"github.com/dockercn/wharf/models"
	"github.com/dockercn/wharf/utils"
)

type OrganizationWebV1Controller struct {
	beego.Controller
}

func (this *OrganizationWebV1Controller) URLMapping() {
	this.Mapping("PostOrganization", this.PostOrganization)
	this.Mapping("PutOrganization", this.PutOrganization)
	this.Mapping("GetOrganizations", this.GetOrganizations)
	this.Mapping("GetOrganizationDetail", this.GetOrganizationDetail)
}

func (this *OrganizationWebV1Controller) Prepare() {
	beego.Debug("[Header] ")
	beego.Debug(this.Ctx.Request.Header)

	this.Ctx.Output.Context.ResponseWriter.Header().Set("Content-Type", "application/json;charset=UTF-8")
}

func (this *OrganizationWebV1Controller) PostOrganization() {

	user, exist := this.Ctx.Input.CruSession.Get("user").(models.User)

	if exist != true {

		beego.Error("[WEB API] Load session failure")

		result := map[string]string{"message": "Session load failure", "url": "/auth"}
		this.Data["json"] = &result

		this.Ctx.Output.Context.Output.SetStatus(http.StatusBadRequest)
		this.ServeJson()
		this.StopRun()

	}

	var org models.Organization

	if err := json.Unmarshal(this.Ctx.Input.CopyBody(), &org); err != nil {

		beego.Error("[WEB API] Unmarshal organization data error:", err.Error())

		result := map[string]string{"message": err.Error()}
		this.Data["json"] = result

		this.Ctx.Output.Context.Output.SetStatus(http.StatusBadRequest)
		this.ServeJson()
		this.StopRun()
	}

	beego.Debug("[WEB API] organization create: %s", string(this.Ctx.Input.CopyBody()))

	org.UUID = utils.GeneralToken(org.Organization)

	org.Username = user.Username

	if err := org.Save(); err != nil {
		beego.Error("[WEB API] Organization save error:", err.Error())

		result := map[string]string{"message": "Organization save error."}
		this.Data["json"] = result

		this.Ctx.Output.Context.Output.SetStatus(http.StatusBadRequest)
		this.ServeJson()
		this.StopRun()
	}

	user.Organizations = append(user.Organizations, org.UUID)

	if err := user.Save(); err != nil {
		beego.Error("[WEB API] User save error:", err.Error())

		result := map[string]string{"message": "User save error."}
		this.Data["json"] = result

		this.Ctx.Output.Context.Output.SetStatus(http.StatusBadRequest)
		this.ServeJson()
		this.StopRun()
	}

	user.Get(user.Username, user.Password)
	this.Ctx.Input.CruSession.Set("user", user)

	result := map[string]string{"message": "Create organization successfully."}
	this.Data["json"] = result

	this.Ctx.Output.Context.Output.SetStatus(http.StatusOK)
	this.ServeJson()
	this.StopRun()
}

func (this *OrganizationWebV1Controller) PutOrganization() {

	if _, exist := this.Ctx.Input.CruSession.Get("user").(models.User); exist != true {

		beego.Error("[WEB API] Load session failure")

		result := map[string]string{"message": "Session load failure", "url": "/auth"}
		this.Data["json"] = &result

		this.Ctx.Output.Context.Output.SetStatus(http.StatusBadRequest)
		this.ServeJson()

	} else {

		var org models.Organization

		if err := json.Unmarshal(this.Ctx.Input.CopyBody(), &org); err != nil {
			beego.Error("[WEB API] Unmarshal organization data error:", err.Error())

			result := map[string]string{"message": err.Error()}
			this.Data["json"] = result

			this.Ctx.Output.Context.Output.SetStatus(http.StatusBadRequest)
			this.ServeJson()
		}

		beego.Debug("[WEB API] organization update: %s", string(this.Ctx.Input.CopyBody()))

		if err := org.Save(); err != nil {
			beego.Error("[WEB API] Organization save error:", err.Error())

			result := map[string]string{"message": "Organization save error."}
			this.Data["json"] = result

			this.Ctx.Output.Context.Output.SetStatus(http.StatusBadRequest)
			this.ServeJson()
		}

		result := map[string]string{"message": "Update organization successfully."}
		this.Data["json"] = result

		this.Ctx.Output.Context.Output.SetStatus(http.StatusOK)
		this.ServeJson()
	}
}

func (this *OrganizationWebV1Controller) GetOrganizations() {
	if user, exist := this.Ctx.Input.CruSession.Get("user").(models.User); exist != true {

		beego.Error("[WEB API] Load session failure")

		result := map[string]string{"message": "Session load failure", "url": "/auth"}
		this.Data["json"] = &result

		this.Ctx.Output.Context.Output.SetStatus(http.StatusBadRequest)
		this.ServeJson()

	} else {

		organizations := make([]models.Organization, len(user.Organizations))

		for i, UUID := range user.Organizations {
			if err := organizations[i].Get(UUID); err != nil {
				beego.Error("[WEB API] Get organizations error:", err.Error())

				result := map[string]string{"message": "Get organizations error."}
				this.Data["json"] = result

				this.Ctx.Output.Context.Output.SetStatus(http.StatusBadRequest)
				this.ServeJson()
			}
		}

		this.Data["json"] = organizations

		this.Ctx.Output.Context.Output.SetStatus(http.StatusOK)
		this.ServeJson()
	}
}

func (this *OrganizationWebV1Controller) GetOrganizationDetail() {
	if _, exist := this.Ctx.Input.CruSession.Get("user").(models.User); exist != true {

		beego.Error("[WEB API] Load session failure")

		result := map[string]string{"message": "Session load failure", "url": "/auth"}
		this.Data["json"] = &result

		this.Ctx.Output.Context.Output.SetStatus(http.StatusBadRequest)
		this.ServeJson()
		this.StopRun()
	} else {
		organization := new(models.Organization)

		if _, _, err := organization.Has(this.Ctx.Input.Param(":org")); err != nil {
			beego.Error("[WEB API] Get organizations error:", err.Error())

			result := map[string]string{"message": "Get organizations error."}
			this.Data["json"] = result

			this.Ctx.Output.Context.Output.SetStatus(http.StatusBadRequest)
			this.ServeJson()
			this.StopRun()
		}

		this.Data["json"] = organization

		this.Ctx.Output.Context.Output.SetStatus(http.StatusOK)
		this.ServeJson()
		this.StopRun()
	}
}

func (this *OrganizationWebV1Controller) GetOrganizationRepo() {

	if _, exist := this.Ctx.Input.CruSession.Get("user").(models.User); exist != true {

		beego.Error("[WEB API] Load session failure")

		result := map[string]string{"message": "Session load failure", "url": "/auth"}
		this.Data["json"] = &result

		this.Ctx.Output.Context.Output.SetStatus(http.StatusBadRequest)
		this.ServeJson()
		this.StopRun()
	}

	org := new(models.Organization)

	if err := org.Get(this.Ctx.Input.Param(":org")); err != nil {
		beego.Error("[WEB API] Load session failure")

		result := map[string]string{"message": "Organization load failure"}
		this.Data["json"] = &result

		this.Ctx.Output.Context.Output.SetStatus(http.StatusBadRequest)
		this.ServeJson()
		this.StopRun()
	}

	repositories := make([]models.Repository, 0)

	for _, repositoryUUID := range org.Repositories {
		repository := new(models.Repository)
		if err := repository.Get(repositoryUUID); err != nil {
			continue
		}
		repositories = append(repositories, *repository)
	}

	this.Data["json"] = repositories

	this.Ctx.Output.Context.Output.SetStatus(http.StatusOK)
	this.ServeJson()
	this.StopRun()
}
<|endoftext|>


<commit_before>package commands

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"os/exec"
	"strings"

	cmds "github.com/ipfs/go-ipfs/commands"
	e "github.com/ipfs/go-ipfs/core/commands/e"
	repo "github.com/ipfs/go-ipfs/repo"
	config "github.com/ipfs/go-ipfs/repo/config"
	fsrepo "github.com/ipfs/go-ipfs/repo/fsrepo"

	"gx/ipfs/QmQp2a2Hhb7F6eK2A5hN8f9aJy4mtkEikL9Zj4cgB7d1dD/go-ipfs-cmdkit"
)

type ConfigField struct {
	Key   string
	Value interface{}
}

var ConfigCmd = &cmds.Command{
	Helptext: cmdkit.HelpText{
		Tagline: "Get and set ipfs config values.",
		ShortDescription: `
'ipfs config' controls configuration variables. It works like 'git config'.
The configuration values are stored in a config file inside your ipfs
repository.`,
		LongDescription: `
'ipfs config' controls configuration variables. It works
much like 'git config'. The configuration values are stored in a config
file inside your IPFS repository.

Examples:

Get the value of the 'Datastore.Path' key:

  $ ipfs config Datastore.Path

Set the value of the 'Datastore.Path' key:

  $ ipfs config Datastore.Path ~/.ipfs/datastore
`,
	},

	Arguments: []cmdkit.Argument{
		cmdkit.StringArg("key", true, false, "The key of the config entry (e.g. \"Addresses.API\")."),
		cmdkit.StringArg("value", false, false, "The value to set the config entry to."),
	},
	Options: []cmdkit.Option{
		cmdkit.BoolOption("bool", "Set a boolean value."),
		cmdkit.BoolOption("json", "Parse stringified JSON."),
	},
	Run: func(req cmds.Request, res cmds.Response) {
		args := req.Arguments()
		key := args[0]

		var output *ConfigField
		defer func() {
			if output != nil {
				res.SetOutput(output)
			} else {
				res.SetOutput(nil)
			}
		}()

		// This is a temporary fix until we move the private key out of the config file
		switch strings.ToLower(key) {
		case "identity", "identity.privkey":
			res.SetError(fmt.Errorf("cannot show or change private key through API"), cmdkit.ErrNormal)
			return
		default:
		}

		r, err := fsrepo.Open(req.InvocContext().ConfigRoot)
		if err != nil {
			res.SetError(err, cmdkit.ErrNormal)
			return
		}
		defer r.Close()
		if len(args) == 2 {
			value := args[1]

			if parseJson, _, _ := req.Option("json").Bool(); parseJson {
				var jsonVal interface{}
				if err := json.Unmarshal([]byte(value), &jsonVal); err != nil {
					err = fmt.Errorf("failed to unmarshal json. %s", err)
					res.SetError(err, cmdkit.ErrNormal)
					return
				}

				output, err = setConfig(r, key, jsonVal)
			} else if isbool, _, _ := req.Option("bool").Bool(); isbool {
				output, err = setConfig(r, key, value == "true")
			} else {
				output, err = setConfig(r, key, value)
			}
		} else {
			output, err = getConfig(r, key)
		}
		if err != nil {
			res.SetError(err, cmdkit.ErrNormal)
			return
		}
	},
	Marshalers: cmds.MarshalerMap{
		cmds.Text: func(res cmds.Response) (io.Reader, error) {
			if len(res.Request().Arguments()) == 2 {
				return nil, nil // dont output anything
			}

			if res.Error() != nil {
				return nil, res.Error()
			}

			v, err := unwrapOutput(res.Output())
			if err != nil {
				return nil, err
			}

			vf, ok := v.(*ConfigField)
			if !ok {
				return nil, e.TypeErr(vf, v)
			}

			buf, err := config.HumanOutput(vf.Value)
			if err != nil {
				return nil, err
			}
			buf = append(buf, byte('\n'))
			return bytes.NewReader(buf), nil
		},
	},
	Type: ConfigField{},
	Subcommands: map[string]*cmds.Command{
		"show":    configShowCmd,
		"edit":    configEditCmd,
		"replace": configReplaceCmd,
		"profile": configProfileCmd,
	},
}

var configShowCmd = &cmds.Command{
	Helptext: cmdkit.HelpText{
		Tagline: "Output config file contents.",
		ShortDescription: `
WARNING: Your private key is stored in the config file, and it will be
included in the output of this command.
`,
	},

	Run: func(req cmds.Request, res cmds.Response) {
		cfgPath := req.InvocContext().ConfigRoot
		fname, err := config.Filename(cfgPath)
		if err != nil {
			res.SetError(err, cmdkit.ErrNormal)
			return
		}

		data, err := ioutil.ReadFile(fname)
		if err != nil {
			res.SetError(err, cmdkit.ErrNormal)
			return
		}

		var cfg map[string]interface{}
		err = json.Unmarshal(data, &cfg)
		if err != nil {
			res.SetError(err, cmdkit.ErrNormal)
			return
		}

		err = scrubValue(cfg, []string{config.IdentityTag, config.PrivKeyTag})
		if err != nil {
			res.SetError(err, cmdkit.ErrNormal)
			return
		}

		output, err := config.HumanOutput(cfg)
		if err != nil {
			res.SetError(err, cmdkit.ErrNormal)
			return
		}

		res.SetOutput(bytes.NewReader(output))
	},
}

func scrubValue(m map[string]interface{}, key []string) error {
	find := func(m map[string]interface{}, k string) (string, interface{}, bool) {
		lckey := strings.ToLower(k)
		for mkey, val := range m {
			lcmkey := strings.ToLower(mkey)
			if lckey == lcmkey {
				return mkey, val, true
			}
		}
		return "", nil, false
	}

	cur := m
	for _, k := range key[:len(key)-1] {
		foundk, val, ok := find(cur, k)
		if !ok {
			return fmt.Errorf("failed to find specified key")
		}

		if foundk != k {
			// case mismatch, calling this an error
			return fmt.Errorf("case mismatch in config, expected %q but got %q", k, foundk)
		}

		mval, mok := val.(map[string]interface{})
		if !mok {
			return fmt.Errorf("%s was not a map", foundk)
		}

		cur = mval
	}

	todel, _, ok := find(cur, key[len(key)-1])
	if !ok {
		return fmt.Errorf("%s, not found", strings.Join(key, "."))
	}

	delete(cur, todel)
	return nil
}

var configEditCmd = &cmds.Command{
	Helptext: cmdkit.HelpText{
		Tagline: "Open the config file for editing in $EDITOR.",
		ShortDescription: `
To use 'ipfs config edit', you must have the $EDITOR environment
variable set to your preferred text editor.
`,
	},

	Run: func(req cmds.Request, res cmds.Response) {
		filename, err := config.Filename(req.InvocContext().ConfigRoot)
		if err != nil {
			res.SetError(err, cmdkit.ErrNormal)
			return
		}

		err = editConfig(filename)
		if err != nil {
			res.SetError(err, cmdkit.ErrNormal)
		}
	},
}

var configReplaceCmd = &cmds.Command{
	Helptext: cmdkit.HelpText{
		Tagline: "Replace the config with <file>.",
		ShortDescription: `
Make sure to back up the config file first if necessary, as this operation
can't be undone.
`,
	},

	Arguments: []cmdkit.Argument{
		cmdkit.FileArg("file", true, false, "The file to use as the new config."),
	},
	Run: func(req cmds.Request, res cmds.Response) {
		// has to be called
		res.SetOutput(nil)

		r, err := fsrepo.Open(req.InvocContext().ConfigRoot)
		if err != nil {
			res.SetError(err, cmdkit.ErrNormal)
			return
		}
		defer r.Close()

		file, err := req.Files().NextFile()
		if err != nil {
			res.SetError(err, cmdkit.ErrNormal)
			return
		}
		defer file.Close()

		err = replaceConfig(r, file)
		if err != nil {
			res.SetError(err, cmdkit.ErrNormal)
			return
		}
	},
}

var configProfileCmd = &cmds.Command{
	Helptext: cmds.HelpText{
		Tagline: "Apply profiles to config.",
	},

	Subcommands: map[string]*cmds.Command{
		"apply":  configProfileApplyCmd,
		"revert": configProfileRevertCmd,
	},
}

var configProfileApplyCmd = &cmds.Command{
	Helptext: cmds.HelpText{
		Tagline: "Apply profile to config.",
	},
	Arguments: []cmds.Argument{
		cmds.StringArg("profile", true, false, "The profile to apply to the config."),
	},
	Run: func(req cmds.Request, res cmds.Response) {
		profile, ok := config.Profiles[req.Arguments()[0]]
		if !ok {
			res.SetError(fmt.Errorf("%s is not a profile", req.Arguments()[0]), cmds.ErrNormal)
			return
		}

		err := transformConfig(req.InvocContext().ConfigRoot, profile.Apply)
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}
	},
}

var configProfileRevertCmd = &cmds.Command{
	Helptext: cmds.HelpText{
		Tagline: "Revert profile changes.",
		ShortDescription: `Reverts profile-related changes to the config.

Reverting some profiles may damage the configuration or not be possible.
Backing up the config before running this command is advised.`,
	},
	Arguments: []cmds.Argument{
		cmds.StringArg("profile", true, false, "The profile to apply to the config."),
	},
	Run: func(req cmds.Request, res cmds.Response) {
		profile, ok := config.Profiles[req.Arguments()[0]]
		if !ok {
			res.SetError(fmt.Errorf("%s is not a profile", req.Arguments()[0]), cmds.ErrNormal)
			return
		}

		err := transformConfig(req.InvocContext().ConfigRoot, profile.Revert)
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}
	},
}

func transformConfig(configRoot string, transformer config.Transformer) error {
	r, err := fsrepo.Open(configRoot)
	if err != nil {
		return err
	}
	defer r.Close()

	cfg, err := r.Config()
	if err != nil {
		return err
	}

	err = transformer(cfg)
	if err != nil {
		return err
	}

	return r.SetConfig(cfg)
}

func getConfig(r repo.Repo, key string) (*ConfigField, error) {
	value, err := r.GetConfigKey(key)
	if err != nil {
		return nil, fmt.Errorf("Failed to get config value: %q", err)
	}
	return &ConfigField{
		Key:   key,
		Value: value,
	}, nil
}

func setConfig(r repo.Repo, key string, value interface{}) (*ConfigField, error) {
	err := r.SetConfigKey(key, value)
	if err != nil {
		return nil, fmt.Errorf("failed to set config value: %s (maybe use --json?)", err)
	}
	return getConfig(r, key)
}

func editConfig(filename string) error {
	editor := os.Getenv("EDITOR")
	if editor == "" {
		return errors.New("ENV variable $EDITOR not set")
	}

	cmd := exec.Command("sh", "-c", editor+" "+filename)
	cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
	return cmd.Run()
}

func replaceConfig(r repo.Repo, file io.Reader) error {
	var cfg config.Config
	if err := json.NewDecoder(file).Decode(&cfg); err != nil {
		return errors.New("failed to decode file as config")
	}
	if len(cfg.Identity.PrivKey) != 0 {
		return errors.New("setting private key with API is not supported")
	}

	keyF, err := getConfig(r, config.PrivKeySelector)
	if err != nil {
		return fmt.Errorf("Failed to get PrivKey")
	}

	pkstr, ok := keyF.Value.(string)
	if !ok {
		return fmt.Errorf("private key in config was not a string")
	}

	cfg.Identity.PrivKey = pkstr

	return r.SetConfig(&cfg)
}
<commit_msg>config-patch: update to new commands lib<commit_after>package commands

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"os/exec"
	"strings"

	cmds "github.com/ipfs/go-ipfs/commands"
	e "github.com/ipfs/go-ipfs/core/commands/e"
	repo "github.com/ipfs/go-ipfs/repo"
	config "github.com/ipfs/go-ipfs/repo/config"
	fsrepo "github.com/ipfs/go-ipfs/repo/fsrepo"

	"gx/ipfs/QmQp2a2Hhb7F6eK2A5hN8f9aJy4mtkEikL9Zj4cgB7d1dD/go-ipfs-cmdkit"
)

type ConfigField struct {
	Key   string
	Value interface{}
}

var ConfigCmd = &cmds.Command{
	Helptext: cmdkit.HelpText{
		Tagline: "Get and set ipfs config values.",
		ShortDescription: `
'ipfs config' controls configuration variables. It works like 'git config'.
The configuration values are stored in a config file inside your ipfs
repository.`,
		LongDescription: `
'ipfs config' controls configuration variables. It works
much like 'git config'. The configuration values are stored in a config
file inside your IPFS repository.

Examples:

Get the value of the 'Datastore.Path' key:

  $ ipfs config Datastore.Path

Set the value of the 'Datastore.Path' key:

  $ ipfs config Datastore.Path ~/.ipfs/datastore
`,
	},

	Arguments: []cmdkit.Argument{
		cmdkit.StringArg("key", true, false, "The key of the config entry (e.g. \"Addresses.API\")."),
		cmdkit.StringArg("value", false, false, "The value to set the config entry to."),
	},
	Options: []cmdkit.Option{
		cmdkit.BoolOption("bool", "Set a boolean value."),
		cmdkit.BoolOption("json", "Parse stringified JSON."),
	},
	Run: func(req cmds.Request, res cmds.Response) {
		args := req.Arguments()
		key := args[0]

		var output *ConfigField
		defer func() {
			if output != nil {
				res.SetOutput(output)
			} else {
				res.SetOutput(nil)
			}
		}()

		// This is a temporary fix until we move the private key out of the config file
		switch strings.ToLower(key) {
		case "identity", "identity.privkey":
			res.SetError(fmt.Errorf("cannot show or change private key through API"), cmdkit.ErrNormal)
			return
		default:
		}

		r, err := fsrepo.Open(req.InvocContext().ConfigRoot)
		if err != nil {
			res.SetError(err, cmdkit.ErrNormal)
			return
		}
		defer r.Close()
		if len(args) == 2 {
			value := args[1]

			if parseJson, _, _ := req.Option("json").Bool(); parseJson {
				var jsonVal interface{}
				if err := json.Unmarshal([]byte(value), &jsonVal); err != nil {
					err = fmt.Errorf("failed to unmarshal json. %s", err)
					res.SetError(err, cmdkit.ErrNormal)
					return
				}

				output, err = setConfig(r, key, jsonVal)
			} else if isbool, _, _ := req.Option("bool").Bool(); isbool {
				output, err = setConfig(r, key, value == "true")
			} else {
				output, err = setConfig(r, key, value)
			}
		} else {
			output, err = getConfig(r, key)
		}
		if err != nil {
			res.SetError(err, cmdkit.ErrNormal)
			return
		}
	},
	Marshalers: cmds.MarshalerMap{
		cmds.Text: func(res cmds.Response) (io.Reader, error) {
			if len(res.Request().Arguments()) == 2 {
				return nil, nil // dont output anything
			}

			if res.Error() != nil {
				return nil, res.Error()
			}

			v, err := unwrapOutput(res.Output())
			if err != nil {
				return nil, err
			}

			vf, ok := v.(*ConfigField)
			if !ok {
				return nil, e.TypeErr(vf, v)
			}

			buf, err := config.HumanOutput(vf.Value)
			if err != nil {
				return nil, err
			}
			buf = append(buf, byte('\n'))
			return bytes.NewReader(buf), nil
		},
	},
	Type: ConfigField{},
	Subcommands: map[string]*cmds.Command{
		"show":    configShowCmd,
		"edit":    configEditCmd,
		"replace": configReplaceCmd,
		"profile": configProfileCmd,
	},
}

var configShowCmd = &cmds.Command{
	Helptext: cmdkit.HelpText{
		Tagline: "Output config file contents.",
		ShortDescription: `
WARNING: Your private key is stored in the config file, and it will be
included in the output of this command.
`,
	},

	Run: func(req cmds.Request, res cmds.Response) {
		cfgPath := req.InvocContext().ConfigRoot
		fname, err := config.Filename(cfgPath)
		if err != nil {
			res.SetError(err, cmdkit.ErrNormal)
			return
		}

		data, err := ioutil.ReadFile(fname)
		if err != nil {
			res.SetError(err, cmdkit.ErrNormal)
			return
		}

		var cfg map[string]interface{}
		err = json.Unmarshal(data, &cfg)
		if err != nil {
			res.SetError(err, cmdkit.ErrNormal)
			return
		}

		err = scrubValue(cfg, []string{config.IdentityTag, config.PrivKeyTag})
		if err != nil {
			res.SetError(err, cmdkit.ErrNormal)
			return
		}

		output, err := config.HumanOutput(cfg)
		if err != nil {
			res.SetError(err, cmdkit.ErrNormal)
			return
		}

		res.SetOutput(bytes.NewReader(output))
	},
}

func scrubValue(m map[string]interface{}, key []string) error {
	find := func(m map[string]interface{}, k string) (string, interface{}, bool) {
		lckey := strings.ToLower(k)
		for mkey, val := range m {
			lcmkey := strings.ToLower(mkey)
			if lckey == lcmkey {
				return mkey, val, true
			}
		}
		return "", nil, false
	}

	cur := m
	for _, k := range key[:len(key)-1] {
		foundk, val, ok := find(cur, k)
		if !ok {
			return fmt.Errorf("failed to find specified key")
		}

		if foundk != k {
			// case mismatch, calling this an error
			return fmt.Errorf("case mismatch in config, expected %q but got %q", k, foundk)
		}

		mval, mok := val.(map[string]interface{})
		if !mok {
			return fmt.Errorf("%s was not a map", foundk)
		}

		cur = mval
	}

	todel, _, ok := find(cur, key[len(key)-1])
	if !ok {
		return fmt.Errorf("%s, not found", strings.Join(key, "."))
	}

	delete(cur, todel)
	return nil
}

var configEditCmd = &cmds.Command{
	Helptext: cmdkit.HelpText{
		Tagline: "Open the config file for editing in $EDITOR.",
		ShortDescription: `
To use 'ipfs config edit', you must have the $EDITOR environment
variable set to your preferred text editor.
`,
	},

	Run: func(req cmds.Request, res cmds.Response) {
		filename, err := config.Filename(req.InvocContext().ConfigRoot)
		if err != nil {
			res.SetError(err, cmdkit.ErrNormal)
			return
		}

		err = editConfig(filename)
		if err != nil {
			res.SetError(err, cmdkit.ErrNormal)
		}
	},
}

var configReplaceCmd = &cmds.Command{
	Helptext: cmdkit.HelpText{
		Tagline: "Replace the config with <file>.",
		ShortDescription: `
Make sure to back up the config file first if necessary, as this operation
can't be undone.
`,
	},

	Arguments: []cmdkit.Argument{
		cmdkit.FileArg("file", true, false, "The file to use as the new config."),
	},
	Run: func(req cmds.Request, res cmds.Response) {
		// has to be called
		res.SetOutput(nil)

		r, err := fsrepo.Open(req.InvocContext().ConfigRoot)
		if err != nil {
			res.SetError(err, cmdkit.ErrNormal)
			return
		}
		defer r.Close()

		file, err := req.Files().NextFile()
		if err != nil {
			res.SetError(err, cmdkit.ErrNormal)
			return
		}
		defer file.Close()

		err = replaceConfig(r, file)
		if err != nil {
			res.SetError(err, cmdkit.ErrNormal)
			return
		}
	},
}

var configProfileCmd = &cmds.Command{
	Helptext: cmdkit.HelpText{
		Tagline: "Apply profiles to config.",
	},

	Subcommands: map[string]*cmds.Command{
		"apply":  configProfileApplyCmd,
		"revert": configProfileRevertCmd,
	},
}

var configProfileApplyCmd = &cmds.Command{
	Helptext: cmdkit.HelpText{
		Tagline: "Apply profile to config.",
	},
	Arguments: []cmdkit.Argument{
		cmdkit.StringArg("profile", true, false, "The profile to apply to the config."),
	},
	Run: func(req cmds.Request, res cmds.Response) {
		profile, ok := config.Profiles[req.Arguments()[0]]
		if !ok {
			res.SetError(fmt.Errorf("%s is not a profile", req.Arguments()[0]), cmdkit.ErrNormal)
			return
		}

		err := transformConfig(req.InvocContext().ConfigRoot, profile.Apply)
		if err != nil {
			res.SetError(err, cmdkit.ErrNormal)
			return
		}
	},
}

var configProfileRevertCmd = &cmds.Command{
	Helptext: cmdkit.HelpText{
		Tagline: "Revert profile changes.",
		ShortDescription: `Reverts profile-related changes to the config.

Reverting some profiles may damage the configuration or not be possible.
Backing up the config before running this command is advised.`,
	},
	Arguments: []cmdkit.Argument{
		cmdkit.StringArg("profile", true, false, "The profile to apply to the config."),
	},
	Run: func(req cmds.Request, res cmds.Response) {
		profile, ok := config.Profiles[req.Arguments()[0]]
		if !ok {
			res.SetError(fmt.Errorf("%s is not a profile", req.Arguments()[0]), cmdkit.ErrNormal)
			return
		}

		err := transformConfig(req.InvocContext().ConfigRoot, profile.Revert)
		if err != nil {
			res.SetError(err, cmdkit.ErrNormal)
			return
		}
	},
}

func transformConfig(configRoot string, transformer config.Transformer) error {
	r, err := fsrepo.Open(configRoot)
	if err != nil {
		return err
	}
	defer r.Close()

	cfg, err := r.Config()
	if err != nil {
		return err
	}

	err = transformer(cfg)
	if err != nil {
		return err
	}

	return r.SetConfig(cfg)
}

func getConfig(r repo.Repo, key string) (*ConfigField, error) {
	value, err := r.GetConfigKey(key)
	if err != nil {
		return nil, fmt.Errorf("Failed to get config value: %q", err)
	}
	return &ConfigField{
		Key:   key,
		Value: value,
	}, nil
}

func setConfig(r repo.Repo, key string, value interface{}) (*ConfigField, error) {
	err := r.SetConfigKey(key, value)
	if err != nil {
		return nil, fmt.Errorf("failed to set config value: %s (maybe use --json?)", err)
	}
	return getConfig(r, key)
}

func editConfig(filename string) error {
	editor := os.Getenv("EDITOR")
	if editor == "" {
		return errors.New("ENV variable $EDITOR not set")
	}

	cmd := exec.Command("sh", "-c", editor+" "+filename)
	cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
	return cmd.Run()
}

func replaceConfig(r repo.Repo, file io.Reader) error {
	var cfg config.Config
	if err := json.NewDecoder(file).Decode(&cfg); err != nil {
		return errors.New("failed to decode file as config")
	}
	if len(cfg.Identity.PrivKey) != 0 {
		return errors.New("setting private key with API is not supported")
	}

	keyF, err := getConfig(r, config.PrivKeySelector)
	if err != nil {
		return fmt.Errorf("Failed to get PrivKey")
	}

	pkstr, ok := keyF.Value.(string)
	if !ok {
		return fmt.Errorf("private key in config was not a string")
	}

	cfg.Identity.PrivKey = pkstr

	return r.SetConfig(&cfg)
}
<|endoftext|>


<commit_before>// Package collaboration provides the logical part for the long running
// operations of collaboration worker
package collaboration

import (
	"bytes"
	"errors"
	"fmt"
	"socialapi/config"
	socialapimodels "socialapi/models"

	"github.com/koding/bongo"

	"socialapi/workers/collaboration/models"
	"strconv"
	"sync"
	"time"

	"github.com/cenkalti/backoff"
	"github.com/koding/kite"
	"github.com/koding/logging"
	"github.com/koding/redis"
	"github.com/streadway/amqp"
)

const (
	// FireEventName is a unique event name for the collaboration ping messages
	FireEventName = "fire"

	// KeyPrefix for redis
	KeyPrefix = "collaboration"
)

var (
	// durations
	//
	// send pings every 15minutes
	pingDuration = time.Second * 15

	// offset should be smaller than a ping
	offsetDuration = time.Second * 10

	// session should be terminated after this duration
	terminateSessionDuration = pingDuration * 4

	// ExpireSessionKeyDuration redis key expiration duration
	ExpireSessionKeyDuration = pingDuration * 5

	// how long the go routine will sleep
	sleepTime = terminateSessionDuration + offsetDuration

	// every go routine should be completed in this duration
	deadLineDuration = time.Minute * 3

	// errors
	//
	errSessionInvalid  = errors.New("session is invalid")
	errDeadlineReached = errors.New("couldnt process the message in deadline")
)

// Controller holds the basic context data for handlers
type Controller struct {
	log   logging.Logger
	redis *redis.RedisSession
	conf  *config.Config
	kite  *kite.Kite
}

// New creates a controller
func New(
	log logging.Logger,
	redis *redis.RedisSession,
	conf *config.Config,
	kite *kite.Kite,
) *Controller {
	return &Controller{
		log:   log,
		redis: redis,
		conf:  conf,
		kite:  kite,
	}
}

// DefaultErrHandler handles the errors for collaboration worker
func (t *Controller) DefaultErrHandler(delivery amqp.Delivery, err error) bool {
	delivery.Nack(false, true)
	return false
}

func validate(ping *models.Ping) error {
	if ping.FileId == "" {
		return fmt.Errorf("fileId is missing %+v", ping)
	}

	if ping.AccountId == 0 {
		return fmt.Errorf("accountId is missing %+v", ping)
	}

	if ping.ChannelId == 0 {
		return fmt.Errorf("channelId is missing %+v", ping)
	}

	return nil
}

// Ping handles the pings coming from client side
func (c *Controller) Ping(ping *models.Ping) error {
	c.log.Debug("new ping %+v", ping)
	if err := validate(ping); err != nil {
		c.log.Error("validation error:%s", err.Error())
		return nil
	}

	err := CanOpen(ping)
	if err != nil && err != socialapimodels.ErrCannotOpenChannel {
		return err
	}

	if err == socialapimodels.ErrCannotOpenChannel {
		return nil
	}

	err = c.checkIfKeyIsValid(ping)
	if err != nil && err != errSessionInvalid {
		c.log.Error("key is not valid %+v", err.Error())
		return err
	}

	if err == errSessionInvalid {
		c.log.Info("session is not valid anymore, collab should be terminated %+v", ping)
		return c.EndSession(ping)
	}

	err = c.wait(ping) // wait syncronously
	if err != nil && err != errSessionInvalid {
		c.log.Error("err while waiting %+v", err)
		return err
	}

	if err == errSessionInvalid {
		c.log.Info("session is not valid anymore, collab should be terminated %+v", ping)
		return c.EndSession(ping)
	}

	c.log.Debug("session is valid %+v", ping)

	return nil
}

func (c *Controller) wait(ping *models.Ping) error {
	select {
	// wait for terminate
	// (session terminate duration + and some offset)
	case <-time.After(sleepTime):
		// check if another ping is set
		// if the key is deleted, it means someone already deleted it
		return c.checkIfKeyIsValid(ping)
	case <-time.After(deadLineDuration):
		return errDeadlineReached
	}
}

func (c *Controller) checkIfKeyIsValid(ping *models.Ping) (err error) {
	defer func() {
		if err != nil {
			c.log.Debug("ping: %+v is not valid err: %+v", ping, err)
		}
	}()

	// check the redis key if it doesnt exist
	key := PrepareFileKey(ping.FileId)
	pingTime, err := c.redis.Get(key)
	if err != nil && err != redis.ErrNil {
		return err
	}

	if err == redis.ErrNil {
		c.log.Debug("redis key not found %+v", ping)
		return errSessionInvalid // key is not there
	}

	unixSec, err := strconv.ParseInt(pingTime, 10, 64)
	if err != nil {
		c.log.Debug("couldn't parse the time", pingTime)

		// discard this case, if the time is invalid, we should not try
		// to process it again
		return errSessionInvalid // key is not valid
	}

	lastPingTimeOnRedis := time.Unix(unixSec, 0).UTC()

	now := time.Now().UTC()

	if now.Add(-terminateSessionDuration).After(lastPingTimeOnRedis) {
		return errSessionInvalid
	}

	// session is valid
	return nil
}

func (c *Controller) EndSession(ping *models.Ping) error {
	var multiErr Error

	defer func() {
		if multiErr != nil {
			c.log.Debug("ping: %+v is not valid %+v err: %+v", ping, multiErr)
		}
	}()

	errChan := make(chan error)
	var wg sync.WaitGroup

	c.goWithRetry(func() error {
		// IMPORTANT
		// 	- DO NOT CHANGE THE ORDER
		//
		toBeRemovedUsers, err := c.findToBeRemovedUsers(ping)
		if err != nil {
			return err
		}

		// then remove them from the db
		if err := c.UnshareVM(ping, toBeRemovedUsers); err != nil {
			return err
		}

		// first remove the users from klient
		if err := c.RemoveUsersFromMachine(ping, toBeRemovedUsers); err != nil {
			return err
		}

		// then end the private messaging
		return c.EndPrivateMessage(ping)
	}, errChan, &wg)

	c.goWithRetry(func() error {
		return c.DeleteDriveDoc(ping)
	}, errChan, &wg)

	go func() {
		// wait until all of them are finised
		wg.Wait()

		// we are done with the operations
		close(errChan)
	}()

	for err := range errChan {
		if err != nil {
			multiErr = append(multiErr, err)
		}
	}

	if len(multiErr) == 0 {
		return nil
	}

	return multiErr
}

func (c *Controller) goWithRetry(f func() error, errChan chan error, wg *sync.WaitGroup) {
	wg.Add(1)
	go func() {
		defer wg.Done()
		bo := backoff.NewExponentialBackOff()
		bo.InitialInterval = time.Millisecond * 250
		bo.MaxInterval = time.Second * 1
		bo.MaxElapsedTime = time.Minute * 2 // channel message can take some time

		ticker := backoff.NewTicker(bo)
		defer ticker.Stop()

		var err error
		for range ticker.C {
			if err = f(); err != nil {
				c.log.Error("err while operating: %s  will retry...", err.Error())
				continue
			}

			break
		}

		errChan <- err
	}()
}

// PrepareFileKey prepares a key for redis
func PrepareFileKey(fileId string) string {
	return fmt.Sprintf(
		"%s:%s:%s",
		config.MustGet().Environment,
		KeyPrefix,
		fileId,
	)
}

func CanOpen(ping *models.Ping) error {
	// fetch the channel
	channel := socialapimodels.NewChannel()
	if err := channel.ById(ping.ChannelId); err != nil {
		// if channel is not there, do not do anyting
		if err == bongo.RecordNotFound {
			return nil
		}

		return err
	}

	canOpen, err := channel.CanOpen(ping.AccountId)
	if err != nil {
		return err
	}

	if !canOpen {
		// if the requester can not open the channel do not process
		return socialapimodels.ErrCannotOpenChannel
	}

	return nil
}

// Error contains error responses.
type Error []error

// Error returns the err string
func (e Error) Error() string {
	var buf bytes.Buffer

	if len(e) == 1 {
		buf.WriteString("1 error: ")
	} else {
		fmt.Fprintf(&buf, "%d errors: ", len(e))
	}

	for i, err := range e {
		if i != 0 {
			buf.WriteString("; ")
		}

		buf.WriteString(err.Error())
	}

	return buf.String()
}
<commit_msg>go/collaboration: remove redis & use mongoCache in controller<commit_after>// Package collaboration provides the logical part for the long running
// operations of collaboration worker
package collaboration

import (
	"bytes"
	"errors"
	"fmt"
	"socialapi/config"
	socialapimodels "socialapi/models"

	"github.com/koding/bongo"
	"github.com/koding/cache"

	"socialapi/workers/collaboration/models"
	"strconv"
	"sync"
	"time"

	"github.com/cenkalti/backoff"
	"github.com/koding/kite"
	"github.com/koding/logging"
	"github.com/streadway/amqp"
)

const (
	// FireEventName is a unique event name for the collaboration ping messages
	FireEventName = "fire"

	// KeyPrefix for redis
	KeyPrefix = "collaboration"
)

var (
	// durations
	//
	// send pings every 15minutes
	pingDuration = time.Second * 15

	// offset should be smaller than a ping
	offsetDuration = time.Second * 10

	// session should be terminated after this duration
	terminateSessionDuration = pingDuration * 4

	// ExpireSessionKeyDuration redis key expiration duration
	ExpireSessionKeyDuration = pingDuration * 5

	// how long the go routine will sleep
	sleepTime = terminateSessionDuration + offsetDuration

	// every go routine should be completed in this duration
	deadLineDuration = time.Minute * 3

	// errors
	//
	errSessionInvalid  = errors.New("session is invalid")
	errDeadlineReached = errors.New("couldnt process the message in deadline")
)

// Controller holds the basic context data for handlers
type Controller struct {
	log        logging.Logger
	mongoCache *cache.MongoCache
	conf       *config.Config
	kite       *kite.Kite
}

// New creates a controller
func New(
	log logging.Logger,
	mongoCache *cache.MongoCache,
	conf *config.Config,
	kite *kite.Kite,
) *Controller {
	return &Controller{
		log:        log,
		mongoCache: mongoCache,
		conf:       conf,
		kite:       kite,
	}
}

// DefaultErrHandler handles the errors for collaboration worker
func (t *Controller) DefaultErrHandler(delivery amqp.Delivery, err error) bool {
	delivery.Nack(false, true)
	return false
}

func validate(ping *models.Ping) error {
	if ping.FileId == "" {
		return fmt.Errorf("fileId is missing %+v", ping)
	}

	if ping.AccountId == 0 {
		return fmt.Errorf("accountId is missing %+v", ping)
	}

	if ping.ChannelId == 0 {
		return fmt.Errorf("channelId is missing %+v", ping)
	}

	return nil
}

// Ping handles the pings coming from client side
func (c *Controller) Ping(ping *models.Ping) error {
	c.log.Debug("new ping %+v", ping)
	if err := validate(ping); err != nil {
		c.log.Error("validation error:%s", err.Error())
		return nil
	}

	err := CanOpen(ping)
	if err != nil && err != socialapimodels.ErrCannotOpenChannel {
		return err
	}

	if err == socialapimodels.ErrCannotOpenChannel {
		return nil
	}

	err = c.checkIfKeyIsValid(ping)
	if err != nil && err != errSessionInvalid {
		c.log.Error("key is not valid %+v", err.Error())
		return err
	}

	if err == errSessionInvalid {
		c.log.Info("session is not valid anymore, collab should be terminated %+v", ping)
		return c.EndSession(ping)
	}

	err = c.wait(ping) // wait syncronously
	if err != nil && err != errSessionInvalid {
		c.log.Error("err while waiting %+v", err)
		return err
	}

	if err == errSessionInvalid {
		c.log.Info("session is not valid anymore, collab should be terminated %+v", ping)
		return c.EndSession(ping)
	}

	c.log.Debug("session is valid %+v", ping)

	return nil
}

func (c *Controller) wait(ping *models.Ping) error {
	select {
	// wait for terminate
	// (session terminate duration + and some offset)
	case <-time.After(sleepTime):
		// check if another ping is set
		// if the key is deleted, it means someone already deleted it
		return c.checkIfKeyIsValid(ping)
	case <-time.After(deadLineDuration):
		return errDeadlineReached
	}
}

func (c *Controller) checkIfKeyIsValid(ping *models.Ping) (err error) {
	defer func() {
		if err != nil {
			c.log.Debug("ping: %+v is not valid err: %+v", ping, err)
		}
	}()

	// check the redis key if it doesnt exist
	key := PrepareFileKey(ping.FileId)
	pingTime, err := c.mongoCache.Get(key)
	if err != nil && err != cache.ErrNotFound {
		return err
	}
	if err == cache.ErrNotFound {
		c.log.Debug("key is not found %+v", ping)
		return errSessionInvalid // key is not there
	}

	pt := fmt.Sprint(pingTime)
	unixSec, err := strconv.ParseInt(pt, 10, 64)
	if err != nil {
		c.log.Debug("couldn't parse the time", pt)

		// discard this case, if the time is invalid, we should not try
		// to process it again
		return errSessionInvalid // key is not valid
	}

	lastPingTime := time.Unix(unixSec, 0).UTC()

	now := time.Now().UTC()

	if now.Add(-terminateSessionDuration).After(lastPingTime) {
		return errSessionInvalid
	}

	// session is valid
	return nil
}

func (c *Controller) EndSession(ping *models.Ping) error {
	var multiErr Error

	defer func() {
		if multiErr != nil {
			c.log.Debug("ping: %+v is not valid %+v err: %+v", ping, multiErr)
		}
	}()

	errChan := make(chan error)
	var wg sync.WaitGroup

	c.goWithRetry(func() error {
		// IMPORTANT
		// 	- DO NOT CHANGE THE ORDER
		//
		toBeRemovedUsers, err := c.findToBeRemovedUsers(ping)
		if err != nil {
			return err
		}

		// then remove them from the db
		if err := c.UnshareVM(ping, toBeRemovedUsers); err != nil {
			return err
		}

		// first remove the users from klient
		if err := c.RemoveUsersFromMachine(ping, toBeRemovedUsers); err != nil {
			return err
		}

		// then end the private messaging
		return c.EndPrivateMessage(ping)
	}, errChan, &wg)

	c.goWithRetry(func() error {
		return c.DeleteDriveDoc(ping)
	}, errChan, &wg)

	go func() {
		// wait until all of them are finised
		wg.Wait()

		// we are done with the operations
		close(errChan)
	}()

	for err := range errChan {
		if err != nil {
			multiErr = append(multiErr, err)
		}
	}

	if len(multiErr) == 0 {
		return nil
	}

	return multiErr
}

func (c *Controller) goWithRetry(f func() error, errChan chan error, wg *sync.WaitGroup) {
	wg.Add(1)
	go func() {
		defer wg.Done()
		bo := backoff.NewExponentialBackOff()
		bo.InitialInterval = time.Millisecond * 250
		bo.MaxInterval = time.Second * 1
		bo.MaxElapsedTime = time.Minute * 2 // channel message can take some time

		ticker := backoff.NewTicker(bo)
		defer ticker.Stop()

		var err error
		for range ticker.C {
			if err = f(); err != nil {
				c.log.Error("err while operating: %s  will retry...", err.Error())
				continue
			}

			break
		}

		errChan <- err
	}()
}

// PrepareFileKey prepares a key for redis
func PrepareFileKey(fileId string) string {
	return fmt.Sprintf(
		"%s:%s:%s",
		config.MustGet().Environment,
		KeyPrefix,
		fileId,
	)
}

func CanOpen(ping *models.Ping) error {
	// fetch the channel
	channel := socialapimodels.NewChannel()
	if err := channel.ById(ping.ChannelId); err != nil {
		// if channel is not there, do not do anyting
		if err == bongo.RecordNotFound {
			return nil
		}

		return err
	}

	canOpen, err := channel.CanOpen(ping.AccountId)
	if err != nil {
		return err
	}

	if !canOpen {
		// if the requester can not open the channel do not process
		return socialapimodels.ErrCannotOpenChannel
	}

	return nil
}

// Error contains error responses.
type Error []error

// Error returns the err string
func (e Error) Error() string {
	var buf bytes.Buffer

	if len(e) == 1 {
		buf.WriteString("1 error: ")
	} else {
		fmt.Fprintf(&buf, "%d errors: ", len(e))
	}

	for i, err := range e {
		if i != 0 {
			buf.WriteString("; ")
		}

		buf.WriteString(err.Error())
	}

	return buf.String()
}
<|endoftext|>