コード例 #1
0
ファイル: api.go プロジェクト: alecu/snappy
func postSnap(c *Command, r *http.Request) Response {
	route := c.d.router.Get(operationCmd.Path)
	if route == nil {
		return InternalError("router can't find route for operation")
	}

	decoder := json.NewDecoder(r.Body)
	var inst snapInstruction
	if err := decoder.Decode(&inst); err != nil {
		return BadRequest("can't decode request body into snap instruction: %v", err)
	}

	vars := muxVars(r)
	inst.pkg = vars["name"] + "." + vars["origin"]

	f := pkgActionDispatch(&inst)
	if f == nil {
		return BadRequest("unknown action %s", inst.Action)
	}

	return AsyncResponse(c.d.AddTask(func() interface{} {
		lock, err := lockfile.Lock(dirs.SnapLockFile, true)
		if err != nil {
			return err
		}
		defer lock.Unlock()
		return f()
	}).Map(route))
}
コード例 #2
0
ファイル: api.go プロジェクト: alecu/snappy
func getLogs(c *Command, r *http.Request) Response {
	vars := muxVars(r)
	name := vars["name"]
	appName := vars["service"]

	lock, err := lockfile.Lock(dirs.SnapLockFile, true)
	if err != nil {
		return InternalError("unable to acquire lock: %v", err)
	}
	defer lock.Unlock()

	actor, err := findServices(name, appName, &progress.NullProgress{})
	if err != nil {
		return NotFound("no services found for %q: %v", name, err)
	}

	rawlogs, err := actor.Logs()
	if err != nil {
		return InternalError("unable to get logs for %q: %v", name, err)
	}

	logs := make([]map[string]interface{}, len(rawlogs))

	for i := range rawlogs {
		logs[i] = map[string]interface{}{
			"timestamp": rawlogs[i].Timestamp(),
			"message":   rawlogs[i].Message(),
			"raw":       rawlogs[i],
		}
	}

	return SyncResponse(logs)
}
コード例 #3
0
func appIconGet(c *Command, r *http.Request) Response {
	vars := muxVars(r)
	name := vars["name"]
	origin := vars["origin"]

	lock, err := lockfile.Lock(dirs.SnapLockFile, true)
	if err != nil {
		return InternalError(err, "Unable to acquire lock")
	}
	defer lock.Unlock()

	bag := lightweight.PartBagByName(name, origin)
	if bag == nil || len(bag.Versions) == 0 {
		return NotFound
	}

	part := bag.LoadBest()
	if part == nil {
		return NotFound
	}

	path := filepath.Clean(part.Icon())
	if !strings.HasPrefix(path, dirs.SnapAppsDir) && !strings.HasPrefix(path, dirs.SnapOemDir) {
		return BadRequest
	}

	return FileResponse(path)
}
コード例 #4
0
ファイル: api.go プロジェクト: alecu/snappy
func iconGet(name, origin string) Response {
	lock, err := lockfile.Lock(dirs.SnapLockFile, true)
	if err != nil {
		return InternalError("unable to acquire lock: %v", err)
	}
	defer lock.Unlock()

	bag := lightweight.PartBagByName(name, origin)
	if bag == nil || len(bag.Versions) == 0 {
		return NotFound("unable to find snap with name %q and origin %q", name, origin)
	}

	part := bag.LoadBest()
	if part == nil {
		return NotFound("unable to load snap with name %q and origin %q", name, origin)
	}

	path := filepath.Clean(part.Icon())
	if !strings.HasPrefix(path, dirs.SnapSnapsDir) {
		// XXX: how could this happen?
		return BadRequest("requested icon is not in snap path")
	}

	return FileResponse(path)
}
コード例 #5
0
ファイル: lockfile_test.go プロジェクト: robert-ancell/snapd
func (ts *FileLockTestSuite) TestFileLockLocks(c *C) {
	path := filepath.Join(c.MkDir(), "lock")
	ch1 := make(chan bool)
	ch2 := make(chan bool)

	go func() {
		ch1 <- true
		lock, err := lockfile.Lock(path, true)
		c.Assert(err, IsNil)
		ch1 <- true
		ch1 <- true
		ch2 <- true
		c.Check(lock.Unlock(), IsNil)
	}()

	go func() {
		<-ch1
		<-ch1
		lock, err := lockfile.Lock(path, false)
		c.Assert(err, Equals, lockfile.ErrAlreadyLocked)
		<-ch1

		lock, err = lockfile.Lock(path, true)
		c.Assert(err, IsNil)
		ch2 <- false
		c.Check(lock.Unlock(), IsNil)
	}()

	var bs []bool
	for {
		select {
		case b := <-ch2:
			bs = append(bs, b)
			if len(bs) == 2 {
				c.Check(bs, DeepEquals, []bool{true, false})
				c.SucceedNow()
			}
		case <-time.After(time.Second):
			c.Fatal("timeout")
		}
	}
}
コード例 #6
0
func configMulti(c *Command, r *http.Request) Response {
	route := c.d.router.Get(operationCmd.Path)
	if route == nil {
		return InternalError(nil, "router can't find route for operation")
	}

	decoder := json.NewDecoder(r.Body)
	var pkgmap map[string]string
	if err := decoder.Decode(&pkgmap); err != nil {
		return BadRequest(err, "can't decode request body into map[string]string: %v", err)
	}

	return AsyncResponse(c.d.AddTask(func() interface{} {
		lock, err := lockfile.Lock(dirs.SnapLockFile, true)
		if err != nil {
			return err
		}
		defer lock.Unlock()

		rspmap := make(map[string]*configSubtask, len(pkgmap))
		bags := lightweight.AllPartBags()
		for pkg, cfg := range pkgmap {
			out := errorResult{}
			sub := configSubtask{Status: TaskFailed, Output: &out}
			rspmap[pkg] = &sub
			bag, ok := bags[pkg]
			if !ok {
				out.Str = snappy.ErrPackageNotFound.Error()
				out.Obj = snappy.ErrPackageNotFound
				continue
			}

			part, _ := bag.Load(bag.ActiveIndex())
			if part == nil {
				out.Str = snappy.ErrSnapNotActive.Error()
				out.Obj = snappy.ErrSnapNotActive
				continue
			}

			config, err := part.Config([]byte(cfg))
			if err != nil {
				out.Msg = "Config failed"
				out.Str = err.Error()
				out.Obj = err
				continue
			}
			sub.Status = TaskSucceeded
			sub.Output = config
		}

		return rspmap
	}).Map(route))
}
コード例 #7
0
ファイル: lockfile_test.go プロジェクト: robert-ancell/snapd
func (ts *FileLockTestSuite) TestFileLock(c *C) {
	path := filepath.Join(c.MkDir(), "lock")

	c.Assert(helpers.FileExists(path), Equals, false)

	lock, err := lockfile.Lock(path, false)
	c.Assert(err, IsNil)
	c.Check(lock > 0, Equals, true)

	c.Assert(helpers.FileExists(path), Equals, true)

	err = lock.Unlock()
	c.Assert(err, IsNil)
}
コード例 #8
0
ファイル: api.go プロジェクト: alecu/snappy
func snapConfig(c *Command, r *http.Request) Response {
	vars := muxVars(r)
	name := vars["name"]
	origin := vars["origin"]
	if name == "" || origin == "" {
		return BadRequest("missing name or origin")
	}
	pkgName := name + "." + origin

	lock, err := lockfile.Lock(dirs.SnapLockFile, true)
	if err != nil {
		return InternalError("unable to acquire lock: %v", err)
	}
	defer lock.Unlock()

	bag := lightweight.PartBagByName(name, origin)
	if bag == nil {
		return NotFound("no snap found with name %q and origin %q", name, origin)
	}

	idx := bag.ActiveIndex()
	if idx < 0 {
		return BadRequest("unable to configure non-active snap")
	}

	part, err := bag.Load(idx)
	if err != nil {
		return InternalError("unable to load active snap: %v", err)
	}

	bs, err := ioutil.ReadAll(r.Body)
	if err != nil {
		return BadRequest("reading config request body gave %v", err)
	}

	overlord := getConfigurator()
	config, err := overlord.Configure(part.(*snappy.SnapPart), bs)
	if err != nil {
		return InternalError("unable to retrieve config for %s: %v", pkgName, err)
	}

	return SyncResponse(config)
}
コード例 #9
0
ファイル: lockfile_test.go プロジェクト: robert-ancell/snapd
func (ts *FileLockTestSuite) TestLockReuseAverted(c *C) {
	dir := c.MkDir()
	path := filepath.Join(dir, "lock")
	lock, err := lockfile.Lock(path, true)
	fd := uintptr(lock) // a copy!
	c.Assert(err, IsNil)

	c.Check(lock, Not(Equals), lockfile.LockedFile(0))
	c.Assert(lock.Unlock(), IsNil)
	c.Check(lock, Equals, lockfile.LockedFile(0))

	f, err := os.Create(filepath.Join(dir, "file"))
	c.Assert(err, IsNil)
	// why os.File.Fd returns an uintptr is a mystery to me
	c.Check(f.Fd(), Equals, fd)

	c.Check(lock.Unlock(), Equals, sys.EBADFD)
	c.Check(f.Sync(), IsNil)
}
コード例 #10
0
func packageConfig(c *Command, r *http.Request) Response {
	vars := muxVars(r)
	name := vars["name"]
	origin := vars["origin"]
	if name == "" || origin == "" {
		return BadRequest(nil, "missing name or origin")
	}
	pkgName := name + "." + origin

	lock, err := lockfile.Lock(dirs.SnapLockFile, true)
	if err != nil {
		return InternalError(err, "Unable to acquire lock")
	}
	defer lock.Unlock()

	bag := lightweight.PartBagByName(name, origin)
	if bag == nil {
		return NotFound
	}

	idx := bag.ActiveIndex()
	if idx < 0 {
		return BadRequest
	}

	part, err := bag.Load(idx)
	if err != nil {
		return InternalError(err, "unable to get load active package: %v", err)
	}

	bs, err := ioutil.ReadAll(r.Body)
	if err != nil {
		return BadRequest(err, "reading config request body gave %v", err)
	}

	config, err := part.Config(bs)
	if err != nil {
		return InternalError(err, "unable to retrieve config for %s: %v", pkgName, err)
	}

	return SyncResponse(config)
}
コード例 #11
0
ファイル: api.go プロジェクト: alecu/snappy
func sysInfo(c *Command, r *http.Request) Response {
	lock, err := lockfile.Lock(dirs.SnapLockFile, true)
	if err != nil {
		return InternalError("unable to acquire lock: %v", err)
	}
	defer lock.Unlock()

	rel := release.Get()
	m := map[string]string{
		"flavor":          rel.Flavor,
		"release":         rel.Series,
		"default_channel": rel.Channel,
		"api_compat":      apiCompatLevel,
	}

	if store := snappy.StoreID(); store != "" {
		m["store"] = store
	}

	return SyncResponse(m)
}
コード例 #12
0
ファイル: api.go プロジェクト: alecu/snappy
func getSnapInfo(c *Command, r *http.Request) Response {
	vars := muxVars(r)
	name := vars["name"]
	origin := vars["origin"]

	lock, err := lockfile.Lock(dirs.SnapLockFile, true)
	if err != nil {
		return InternalError("unable to acquire lock: %v", err)
	}
	defer lock.Unlock()

	repo := newRemoteRepo()
	var part snappy.Part
	if parts, _ := repo.Details(name, origin); len(parts) > 0 {
		part = parts[0]
	}

	bag := lightweight.PartBagByName(name, origin)
	if bag == nil && part == nil {
		return NotFound("unable to find snap with name %q and origin %q", name, origin)
	}

	route := c.d.router.Get(c.Path)
	if route == nil {
		return InternalError("router can't find route for snap %s.%s", name, origin)
	}

	url, err := route.URL("name", name, "origin", origin)
	if err != nil {
		return InternalError("route can't build URL for snap %s.%s: %v", name, origin, err)
	}

	result := webify(bag.Map(part), url.String())

	return SyncResponse(result)
}
コード例 #13
0
ファイル: api.go プロジェクト: alecu/snappy
func sideloadSnap(c *Command, r *http.Request) Response {
	route := c.d.router.Get(operationCmd.Path)
	if route == nil {
		return InternalError("router can't find route for operation")
	}

	body := r.Body
	unsignedOk := false
	contentType := r.Header.Get("Content-Type")

	if strings.HasPrefix(contentType, "multipart/") {
		// spec says POSTs to sideload snaps should be “a multipart file upload”

		_, params, err := mime.ParseMediaType(contentType)
		if err != nil {
			return BadRequest("unable to parse POST body: %v", err)
		}

		form, err := multipart.NewReader(r.Body, params["boundary"]).ReadForm(maxReadBuflen)
		if err != nil {
			return BadRequest("unable to read POST form: %v", err)
		}

		// if allow-unsigned is present in the form, unsigned is OK
		_, unsignedOk = form.Value["allow-unsigned"]

		// form.File is a map of arrays of *FileHeader things
		// we just allow one (for now at least)
	out:
		for _, v := range form.File {
			for i := range v {
				body, err = v[i].Open()
				if err != nil {
					return BadRequest("unable to open POST form file: %v", err)
				}
				defer body.Close()

				break out
			}
		}
		defer form.RemoveAll()
	} else {
		// Looks like user didn't understand that multipart thing.
		// Maybe they just POSTed the snap at us (quite handy to do with e.g. curl).
		// So we try that.

		// If x-allow-unsigned is present, unsigned is OK
		_, unsignedOk = r.Header["X-Allow-Unsigned"]
	}

	tmpf, err := ioutil.TempFile("", "snapd-sideload-pkg-")
	if err != nil {
		return InternalError("can't create tempfile: %v", err)
	}

	if _, err := io.Copy(tmpf, body); err != nil {
		os.Remove(tmpf.Name())
		return InternalError("can't copy request into tempfile: %v", err)
	}

	return AsyncResponse(c.d.AddTask(func() interface{} {
		defer os.Remove(tmpf.Name())

		_, err := newSnap(tmpf.Name(), snappy.SideloadedOrigin, unsignedOk)
		if err != nil {
			return err
		}

		lock, err := lockfile.Lock(dirs.SnapLockFile, true)
		if err != nil {
			return err
		}
		defer lock.Unlock()

		var flags snappy.InstallFlags
		if unsignedOk {
			flags |= snappy.AllowUnauthenticated
		}
		overlord := &snappy.Overlord{}
		name, err := overlord.Install(tmpf.Name(), snappy.SideloadedOrigin, flags, &progress.NullProgress{})
		if err != nil {
			return err
		}

		return name
	}).Map(route))
}
コード例 #14
0
// plural!
func getPackagesInfo(c *Command, r *http.Request) Response {
	route := c.d.router.Get(packageCmd.Path)
	if route == nil {
		return InternalError(nil, "router can't find route for packages")
	}

	lock, err := lockfile.Lock(dirs.SnapLockFile, true)
	if err != nil {
		return InternalError(err, "Unable to acquire lock")
	}
	defer lock.Unlock()

	sources := make([]string, 1, 3)
	sources[0] = "local"
	// we're not worried if the remote repos error out
	found, _ := newRemoteRepo().All()
	if len(found) > 0 {
		sources = append(sources, "store")
	}

	upd, _ := newSystemRepo().Updates()
	if len(upd) > 0 {
		sources = append(sources, "system-image")
	}

	found = append(found, upd...)

	sort.Sort(byQN(found))

	bags := lightweight.AllPartBags()

	results := make(map[string]map[string]string)
	for _, part := range found {
		name := part.Name()
		origin := part.Origin()

		url, err := route.URL("name", name, "origin", origin)
		if err != nil {
			return InternalError(err, "can't get route to details for %s.%s: %v", name, origin, err)
		}

		fullname := name + "." + origin
		qn := snappy.QualifiedName(part)
		results[fullname] = webify(bags[qn].Map(part), url.String())
		delete(bags, qn)
	}

	for _, v := range bags {
		m := v.Map(nil)
		name := m["name"]
		origin := m["origin"]

		resource := "no resource URL for this resource"
		url, _ := route.URL("name", name, "origin", origin)
		if url != nil {
			resource = url.String()
		}

		results[name+"."+origin] = webify(m, resource)
	}

	return SyncResponse(map[string]interface{}{
		"packages": results,
		"sources":  sources,
		"paging": map[string]interface{}{
			"pages": 1,
			"page":  1,
			"count": len(results),
		},
	})
}
コード例 #15
0
ファイル: api.go プロジェクト: alecu/snappy
func snapService(c *Command, r *http.Request) Response {
	route := c.d.router.Get(operationCmd.Path)
	if route == nil {
		return InternalError("router can't find route for operation")
	}

	vars := muxVars(r)
	name := vars["name"]
	origin := vars["origin"]
	if name == "" || origin == "" {
		return BadRequest("missing name or origin")
	}
	appName := vars["service"]
	pkgName := name + "." + origin

	action := "status"

	if r.Method != "GET" {
		decoder := json.NewDecoder(r.Body)
		var cmd map[string]string
		if err := decoder.Decode(&cmd); err != nil {
			return BadRequest("can't decode request body into service command: %v", err)
		}

		action = cmd["action"]
	}

	var lock lockfile.LockedFile
	reachedAsync := false
	switch action {
	case "status", "start", "stop", "restart", "enable", "disable":
		var err error
		lock, err = lockfile.Lock(dirs.SnapLockFile, true)

		if err != nil {
			return InternalError("unable to acquire lock: %v", err)
		}

		defer func() {
			if !reachedAsync {
				lock.Unlock()
			}
		}()
	default:
		return BadRequest("unknown action %s", action)
	}

	bag := lightweight.PartBagByName(name, origin)
	idx := bag.ActiveIndex()
	if idx < 0 {
		return NotFound("unable to find snap with name %q and origin %q", name, origin)
	}

	ipart, err := bag.Load(idx)
	if err != nil {
		return InternalError("unable to load active snap: %v", err)
	}

	part, ok := ipart.(*snappy.SnapPart)
	if !ok {
		return InternalError("active snap is not a *snappy.SnapPart: %T", ipart)
	}
	apps := part.Apps()

	if len(apps) == 0 {
		return NotFound("snap %q has no services", pkgName)
	}

	appmap := make(map[string]*appDesc, len(apps))
	for i := range apps {
		if apps[i].Daemon == "" {
			continue
		}
		appmap[apps[i].Name] = &appDesc{Spec: apps[i], Op: action}
	}

	if appName != "" && appmap[appName] == nil {
		return NotFound("snap %q has no service %q", pkgName, appName)
	}

	// note findServices takes the *bare* name
	actor, err := findServices(name, appName, &progress.NullProgress{})
	if err != nil {
		return InternalError("no services for %q [%q] found: %v", pkgName, appName, err)
	}

	f := func() interface{} {
		status, err := actor.ServiceStatus()
		if err != nil {
			logger.Noticef("unable to get status for %q [%q]: %v", pkgName, appName, err)
			return err
		}

		for i := range status {
			if desc, ok := appmap[status[i].ServiceName]; ok {
				desc.Status = status[i]
			} else {
				// shouldn't really happen, but can't hurt
				appmap[status[i].ServiceName] = &appDesc{Status: status[i]}
			}
		}

		if appName == "" {
			return appmap
		}

		return appmap[appName]
	}

	if action == "status" {
		return SyncResponse(f())
	}

	reachedAsync = true

	return AsyncResponse(c.d.AddTask(func() interface{} {
		defer lock.Unlock()

		switch action {
		case "start":
			err = actor.Start()
		case "stop":
			err = actor.Stop()
		case "enable":
			err = actor.Enable()
		case "disable":
			err = actor.Disable()
		case "restart":
			err = actor.Restart()
		}

		if err != nil {
			logger.Noticef("unable to %s %q [%q]: %v\n", action, pkgName, appName, err)
			return err
		}

		return f()
	}).Map(route))
}
コード例 #16
0
ファイル: api.go プロジェクト: alecu/snappy
// plural!
func getSnapsInfo(c *Command, r *http.Request) Response {
	route := c.d.router.Get(snapCmd.Path)
	if route == nil {
		return InternalError("router can't find route for snaps")
	}

	lock, err := lockfile.Lock(dirs.SnapLockFile, true)
	if err != nil {
		return InternalError("unable to acquire lock: %v", err)
	}
	defer lock.Unlock()

	// TODO: Marshal incrementally leveraging json.RawMessage.
	results := make(map[string]map[string]interface{})
	sources := make([]string, 0, 2)
	query := r.URL.Query()

	var includeStore, includeLocal bool
	if len(query["sources"]) > 0 {
		for _, v := range strings.Split(query["sources"][0], ",") {
			if v == "store" {
				includeStore = true
			} else if v == "local" {
				includeLocal = true
			}
		}
	} else {
		includeStore = true
		includeLocal = true
	}

	searchTerm := query.Get("q")

	var includeTypes []string
	if len(query["types"]) > 0 {
		includeTypes = strings.Split(query["types"][0], ",")
	}

	var bags map[string]*lightweight.PartBag

	if includeLocal {
		sources = append(sources, "local")
		bags = lightweight.AllPartBags()

		for _, v := range bags {
			m := v.Map(nil)
			name, _ := m["name"].(string)
			origin, _ := m["origin"].(string)

			resource := "no resource URL for this resource"
			url, err := route.URL("name", name, "origin", origin)
			if err == nil {
				resource = url.String()
			}

			fullname := name + "." + origin

			// strings.Contains(fullname, "") is true
			if !strings.Contains(fullname, searchTerm) {
				continue
			}

			results[fullname] = webify(m, resource)
		}
	}

	if includeStore {
		repo := newRemoteRepo()
		var found []snappy.Part

		// repo.Find("") finds all
		//
		// TODO: Instead of ignoring the error from Find:
		//   * if there are no results, return an error response.
		//   * If there are results at all (perhaps local), include a
		//     warning in the response
		found, _ = repo.Find(searchTerm)

		sources = append(sources, "store")

		sort.Sort(byQN(found))

		for _, part := range found {
			name := part.Name()
			origin := part.Origin()

			url, err := route.URL("name", name, "origin", origin)
			if err != nil {
				return InternalError("can't get route to details for %s.%s: %v", name, origin, err)
			}

			fullname := name + "." + origin
			qn := snappy.QualifiedName(part)
			results[fullname] = webify(bags[qn].Map(part), url.String())
		}
	}

	// TODO: it should be possible to search on	the "content" field on the store
	//       with multiple values, see:
	//       https://wiki.ubuntu.com/AppStore/Interfaces/ClickPackageIndex#Search
	if len(includeTypes) > 0 {
		for name, result := range results {
			if !resultHasType(result, includeTypes) {
				delete(results, name)
			}
		}
	}

	return SyncResponse(map[string]interface{}{
		"snaps":   results,
		"sources": sources,
		"paging": map[string]interface{}{
			"pages": 1,
			"page":  1,
			"count": len(results),
		},
	})
}
コード例 #17
0
ファイル: api.go プロジェクト: robert-ancell/snapd
// plural!
func getSnapsInfo(c *Command, r *http.Request) Response {
	route := c.d.router.Get(snapCmd.Path)
	if route == nil {
		return InternalError("router can't find route for snaps")
	}

	lock, err := lockfile.Lock(dirs.SnapLockFile, true)
	if err != nil {
		return InternalError("unable to acquire lock: %v", err)
	}
	defer lock.Unlock()

	// TODO: Marshal incrementally leveraging json.RawMessage.
	results := make(map[string]map[string]interface{})
	sources := make([]string, 0, 2)
	query := r.URL.Query()

	var includeStore, includeLocal bool
	if len(query["sources"]) > 0 {
		for _, v := range strings.Split(query["sources"][0], ",") {
			if v == "store" {
				includeStore = true
			} else if v == "local" {
				includeLocal = true
			}
		}
	} else {
		includeStore = true
		includeLocal = true
	}

	var bags map[string]*lightweight.PartBag

	if includeLocal {
		sources = append(sources, "local")
		bags = lightweight.AllPartBags()

		for _, v := range bags {
			m := v.Map(nil)
			name, _ := m["name"].(string)
			origin, _ := m["origin"].(string)

			resource := "no resource URL for this resource"
			url, err := route.URL("name", name, "origin", origin)
			if err == nil {
				resource = url.String()
			}

			results[name+"."+origin] = webify(m, resource)
		}
	}

	if includeStore {
		// TODO: If there are no results (local or remote), report the error. If
		//       there are results at all, inform that the result is partial.
		found, _ := newRemoteRepo().All()
		if len(found) > 0 {
			sources = append(sources, "store")
		}

		sort.Sort(byQN(found))

		for _, part := range found {
			name := part.Name()
			origin := part.Origin()

			url, err := route.URL("name", name, "origin", origin)
			if err != nil {
				return InternalError("can't get route to details for %s.%s: %v", name, origin, err)
			}

			fullname := name + "." + origin
			qn := snappy.QualifiedName(part)
			results[fullname] = webify(bags[qn].Map(part), url.String())
		}
	}

	return SyncResponse(map[string]interface{}{
		"snaps":   results,
		"sources": sources,
		"paging": map[string]interface{}{
			"pages": 1,
			"page":  1,
			"count": len(results),
		},
	})
}