Java/Android libuplink bindings (#1918)
38
internal/fpath/temp_data.go
Normal file
@ -0,0 +1,38 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
// TODO maybe there is better place for this
|
||||
|
||||
package fpath
|
||||
|
||||
import "context"
|
||||
|
||||
// The key type is unexported to prevent collisions with context keys defined in
|
||||
// other packages.
|
||||
type key int
|
||||
|
||||
// temp is the context key for temp struct
|
||||
const tempKey key = 0
|
||||
|
||||
type temp struct {
|
||||
inmemory bool
|
||||
directory string
|
||||
}
|
||||
|
||||
// WithTempData creates context with information how store temporary data, in memory or on disk
|
||||
func WithTempData(ctx context.Context, directory string, inmemory bool) context.Context {
|
||||
temp := &temp{
|
||||
inmemory: inmemory,
|
||||
directory: directory,
|
||||
}
|
||||
return context.WithValue(ctx, tempKey, temp)
|
||||
}
|
||||
|
||||
// GetTempData returns if temporary data should be stored in memory or on disk
|
||||
func GetTempData(ctx context.Context) (string, bool, bool) {
|
||||
tempValue, ok := ctx.Value(tempKey).(temp)
|
||||
if !ok {
|
||||
return "", false, false
|
||||
}
|
||||
return tempValue.directory, tempValue.inmemory, ok
|
||||
}
|
@ -42,6 +42,13 @@ func NewTeeFile(readers int, tempdir string) ([]PipeReader, PipeWriter, error) {
|
||||
return newTee(buffer, readers, &handles)
|
||||
}
|
||||
|
||||
// NewTeeInmemory returns a tee that uses inmemory
|
||||
func NewTeeInmemory(readers int, allocMemory int64) ([]PipeReader, PipeWriter, error) {
|
||||
handles := int64(readers + 1) // +1 for the writer
|
||||
memory := memory(make([]byte, allocMemory))
|
||||
return newTee(memory, readers, &handles)
|
||||
}
|
||||
|
||||
func newTee(buffer ReadAtWriteAtCloser, readers int, open *int64) ([]PipeReader, PipeWriter, error) {
|
||||
tee := &tee{
|
||||
buffer: buffer,
|
||||
|
@ -12,11 +12,19 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"storj.io/storj/internal/memory"
|
||||
"storj.io/storj/internal/sync2"
|
||||
)
|
||||
|
||||
func TestTee_Basic(t *testing.T) {
|
||||
testTees(t, func(t *testing.T, readers []sync2.PipeReader, writer sync2.PipeWriter) {
|
||||
func TestTee_Basic_On_Disk(t *testing.T) {
|
||||
testTees(t, false, testBasic)
|
||||
}
|
||||
|
||||
func TestTee_Basic_In_Memory(t *testing.T) {
|
||||
testTees(t, true, testBasic)
|
||||
}
|
||||
|
||||
func testBasic(t *testing.T, readers []sync2.PipeReader, writer sync2.PipeWriter) {
|
||||
var group errgroup.Group
|
||||
group.Go(func() error {
|
||||
n, err := writer.Write([]byte{1, 2, 3})
|
||||
@ -45,11 +53,10 @@ func TestTee_Basic(t *testing.T) {
|
||||
}
|
||||
|
||||
assert.NoError(t, group.Wait())
|
||||
})
|
||||
}
|
||||
|
||||
func TestTee_CloseWithError(t *testing.T) {
|
||||
testTees(t, func(t *testing.T, readers []sync2.PipeReader, writer sync2.PipeWriter) {
|
||||
testTees(t, false, func(t *testing.T, readers []sync2.PipeReader, writer sync2.PipeWriter) {
|
||||
var failure = errors.New("write failure")
|
||||
|
||||
var group errgroup.Group
|
||||
@ -80,11 +87,19 @@ func TestTee_CloseWithError(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func testTees(t *testing.T, test func(t *testing.T, readers []sync2.PipeReader, writer sync2.PipeWriter)) {
|
||||
func testTees(t *testing.T, inmemory bool, test func(t *testing.T, readers []sync2.PipeReader, writer sync2.PipeWriter)) {
|
||||
t.Parallel()
|
||||
t.Run("File", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
readers, writer, err := sync2.NewTeeFile(2, "")
|
||||
|
||||
var err error
|
||||
var readers []sync2.PipeReader
|
||||
var writer sync2.PipeWriter
|
||||
if inmemory {
|
||||
readers, writer, err = sync2.NewTeeInmemory(2, memory.MiB.Int64())
|
||||
} else {
|
||||
readers, writer, err = sync2.NewTeeFile(2, "")
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -96,6 +96,38 @@ type UploadOptions struct {
|
||||
func (b *Bucket) UploadObject(ctx context.Context, path storj.Path, data io.Reader, opts *UploadOptions) (err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
upload, err := b.NewWriter(ctx, path, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = io.Copy(upload, data)
|
||||
|
||||
return errs.Combine(err, upload.Close())
|
||||
}
|
||||
|
||||
// DeleteObject removes an object, if authorized.
|
||||
func (b *Bucket) DeleteObject(ctx context.Context, path storj.Path) (err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
return b.metainfo.DeleteObject(ctx, b.bucket.Name, path)
|
||||
}
|
||||
|
||||
// ListOptions controls options for the ListObjects() call.
|
||||
type ListOptions = storj.ListOptions
|
||||
|
||||
// ListObjects lists objects a user is authorized to see.
|
||||
func (b *Bucket) ListObjects(ctx context.Context, cfg *ListOptions) (list storj.ObjectList, err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
if cfg == nil {
|
||||
cfg = &storj.ListOptions{}
|
||||
}
|
||||
return b.metainfo.ListObjects(ctx, b.bucket.Name, *cfg)
|
||||
}
|
||||
|
||||
// NewWriter creates a writer which uploads the object.
|
||||
func (b *Bucket) NewWriter(ctx context.Context, path storj.Path, opts *UploadOptions) (_ io.WriteCloser, err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
if opts == nil {
|
||||
opts = &UploadOptions{}
|
||||
}
|
||||
@ -134,37 +166,35 @@ func (b *Bucket) UploadObject(ctx context.Context, path storj.Path, data io.Read
|
||||
|
||||
obj, err := b.metainfo.CreateObject(ctx, b.Name, path, &createInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mutableStream, err := obj.CreateStream(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
upload := stream.NewUpload(ctx, mutableStream, b.streams)
|
||||
|
||||
_, err = io.Copy(upload, data)
|
||||
|
||||
return errs.Combine(err, upload.Close())
|
||||
return upload, nil
|
||||
}
|
||||
|
||||
// DeleteObject removes an object, if authorized.
|
||||
func (b *Bucket) DeleteObject(ctx context.Context, path storj.Path) (err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
return b.metainfo.DeleteObject(ctx, b.bucket.Name, path)
|
||||
// ReadSeekCloser combines interfaces io.Reader, io.Seeker, io.Closer
|
||||
type ReadSeekCloser interface {
|
||||
io.Reader
|
||||
io.Seeker
|
||||
io.Closer
|
||||
}
|
||||
|
||||
// ListOptions controls options for the ListObjects() call.
|
||||
type ListOptions = storj.ListOptions
|
||||
|
||||
// ListObjects lists objects a user is authorized to see.
|
||||
func (b *Bucket) ListObjects(ctx context.Context, cfg *ListOptions) (list storj.ObjectList, err error) {
|
||||
// NewReader creates a new reader that downloads the object data.
|
||||
func (b *Bucket) NewReader(ctx context.Context, path storj.Path) (_ ReadSeekCloser, err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
if cfg == nil {
|
||||
cfg = &storj.ListOptions{}
|
||||
|
||||
segmentStream, err := b.metainfo.GetObjectStream(ctx, b.Name, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b.metainfo.ListObjects(ctx, b.bucket.Name, *cfg)
|
||||
|
||||
return stream.NewDownload(ctx, segmentStream, b.streams), nil
|
||||
}
|
||||
|
||||
// Close closes the Bucket session.
|
||||
|
3
mobile/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
.gradle
|
||||
libuplink_android/app/libs/*.jar
|
||||
libuplink_android/app/libs/*.aar
|
376
mobile/bucket.go
Normal file
@ -0,0 +1,376 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
package mobile
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
libuplink "storj.io/storj/lib/uplink"
|
||||
"storj.io/storj/pkg/storj"
|
||||
)
|
||||
|
||||
const (
|
||||
// CipherSuiteEncUnspecified indicates no encryption suite has been selected.
|
||||
CipherSuiteEncUnspecified = byte(storj.EncUnspecified)
|
||||
// CipherSuiteEncNull indicates use of the NULL cipher; that is, no encryption is
|
||||
// done. The ciphertext is equal to the plaintext.
|
||||
CipherSuiteEncNull = byte(storj.EncNull)
|
||||
// CipherSuiteEncAESGCM indicates use of AES128-GCM encryption.
|
||||
CipherSuiteEncAESGCM = byte(storj.EncAESGCM)
|
||||
// CipherSuiteEncSecretBox indicates use of XSalsa20-Poly1305 encryption, as provided
|
||||
// by the NaCl cryptography library under the name "Secretbox".
|
||||
CipherSuiteEncSecretBox = byte(storj.EncSecretBox)
|
||||
|
||||
// DirectionAfter lists forwards from cursor, without cursor
|
||||
DirectionAfter = int(storj.After)
|
||||
// DirectionForward lists forwards from cursor, including cursor
|
||||
DirectionForward = int(storj.Forward)
|
||||
// DirectionBackward lists backwards from cursor, including cursor
|
||||
DirectionBackward = int(storj.Backward)
|
||||
// DirectionBefore lists backwards from cursor, without cursor
|
||||
DirectionBefore = int(storj.Before)
|
||||
)
|
||||
|
||||
// Bucket represents operations you can perform on a bucket
|
||||
type Bucket struct {
|
||||
Name string
|
||||
|
||||
scope
|
||||
lib *libuplink.Bucket
|
||||
}
|
||||
|
||||
// BucketAccess defines access to bucket
|
||||
type BucketAccess struct {
|
||||
PathEncryptionKey []byte
|
||||
EncryptedPathPrefix storj.Path
|
||||
}
|
||||
|
||||
// BucketInfo bucket meta struct
|
||||
type BucketInfo struct {
|
||||
Name string
|
||||
Created int64
|
||||
PathCipher byte
|
||||
SegmentsSize int64
|
||||
RedundancyScheme *RedundancyScheme
|
||||
EncryptionParameters *EncryptionParameters
|
||||
}
|
||||
|
||||
func newBucketInfo(bucket storj.Bucket) *BucketInfo {
|
||||
return &BucketInfo{
|
||||
Name: bucket.Name,
|
||||
Created: bucket.Created.UTC().UnixNano() / int64(time.Millisecond),
|
||||
PathCipher: byte(bucket.PathCipher),
|
||||
SegmentsSize: bucket.SegmentsSize,
|
||||
RedundancyScheme: &RedundancyScheme{
|
||||
Algorithm: byte(bucket.RedundancyScheme.Algorithm),
|
||||
ShareSize: bucket.RedundancyScheme.ShareSize,
|
||||
RequiredShares: bucket.RedundancyScheme.RequiredShares,
|
||||
RepairShares: bucket.RedundancyScheme.RepairShares,
|
||||
OptimalShares: bucket.RedundancyScheme.OptimalShares,
|
||||
TotalShares: bucket.RedundancyScheme.TotalShares,
|
||||
},
|
||||
EncryptionParameters: &EncryptionParameters{
|
||||
CipherSuite: byte(bucket.EncryptionParameters.CipherSuite),
|
||||
BlockSize: bucket.EncryptionParameters.BlockSize,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// BucketConfig bucket configuration
|
||||
type BucketConfig struct {
|
||||
// PathCipher indicates which cipher suite is to be used for path
|
||||
// encryption within the new Bucket. If not set, AES-GCM encryption
|
||||
// will be used.
|
||||
PathCipher byte
|
||||
|
||||
// EncryptionParameters specifies the default encryption parameters to
|
||||
// be used for data encryption of new Objects in this bucket.
|
||||
EncryptionParameters *EncryptionParameters
|
||||
|
||||
// RedundancyScheme defines the default Reed-Solomon and/or
|
||||
// Forward Error Correction encoding parameters to be used by
|
||||
// objects in this Bucket.
|
||||
RedundancyScheme *RedundancyScheme
|
||||
// SegmentsSize is the default segment size to use for new
|
||||
// objects in this Bucket.
|
||||
SegmentsSize int64
|
||||
}
|
||||
|
||||
// BucketList is a list of buckets
|
||||
type BucketList struct {
|
||||
list storj.BucketList
|
||||
}
|
||||
|
||||
// More returns true if list request was not able to return all results
|
||||
func (bl *BucketList) More() bool {
|
||||
return bl.list.More
|
||||
}
|
||||
|
||||
// Length returns number of returned items
|
||||
func (bl *BucketList) Length() int {
|
||||
return len(bl.list.Items)
|
||||
}
|
||||
|
||||
// Item gets item from specific index
|
||||
func (bl *BucketList) Item(index int) (*BucketInfo, error) {
|
||||
if index < 0 && index >= len(bl.list.Items) {
|
||||
return nil, fmt.Errorf("index out of range")
|
||||
}
|
||||
return newBucketInfo(bl.list.Items[index]), nil
|
||||
}
|
||||
|
||||
// RedundancyScheme specifies the parameters and the algorithm for redundancy
|
||||
type RedundancyScheme struct {
|
||||
// Algorithm determines the algorithm to be used for redundancy.
|
||||
Algorithm byte
|
||||
|
||||
// ShareSize is the size to use for new redundancy shares.
|
||||
ShareSize int32
|
||||
|
||||
// RequiredShares is the minimum number of shares required to recover a
|
||||
// segment.
|
||||
RequiredShares int16
|
||||
// RepairShares is the minimum number of safe shares that can remain
|
||||
// before a repair is triggered.
|
||||
RepairShares int16
|
||||
// OptimalShares is the desired total number of shares for a segment.
|
||||
OptimalShares int16
|
||||
// TotalShares is the number of shares to encode. If it is larger than
|
||||
// OptimalShares, slower uploads of the excess shares will be aborted in
|
||||
// order to improve performance.
|
||||
TotalShares int16
|
||||
}
|
||||
|
||||
func newStorjRedundancyScheme(scheme *RedundancyScheme) storj.RedundancyScheme {
|
||||
if scheme == nil {
|
||||
return storj.RedundancyScheme{}
|
||||
}
|
||||
return storj.RedundancyScheme{
|
||||
Algorithm: storj.RedundancyAlgorithm(scheme.Algorithm),
|
||||
ShareSize: scheme.ShareSize,
|
||||
RequiredShares: scheme.RequiredShares,
|
||||
RepairShares: scheme.RepairShares,
|
||||
OptimalShares: scheme.OptimalShares,
|
||||
TotalShares: scheme.TotalShares,
|
||||
}
|
||||
}
|
||||
|
||||
// EncryptionParameters is the cipher suite and parameters used for encryption
|
||||
// It is like EncryptionScheme, but uses the CipherSuite type instead of Cipher.
|
||||
// EncryptionParameters is preferred for new uses.
|
||||
type EncryptionParameters struct {
|
||||
// CipherSuite specifies the cipher suite to be used for encryption.
|
||||
CipherSuite byte
|
||||
// BlockSize determines the unit size at which encryption is performed.
|
||||
// It is important to distinguish this from the block size used by the
|
||||
// cipher suite (probably 128 bits). There is some small overhead for
|
||||
// each encryption unit, so BlockSize should not be too small, but
|
||||
// smaller sizes yield shorter first-byte latency and better seek times.
|
||||
// Note that BlockSize itself is the size of data blocks _after_ they
|
||||
// have been encrypted and the authentication overhead has been added.
|
||||
// It is _not_ the size of the data blocks to _be_ encrypted.
|
||||
BlockSize int32
|
||||
}
|
||||
|
||||
func newStorjEncryptionParameters(ec *EncryptionParameters) storj.EncryptionParameters {
|
||||
if ec == nil {
|
||||
return storj.EncryptionParameters{}
|
||||
}
|
||||
return storj.EncryptionParameters{
|
||||
CipherSuite: storj.CipherSuite(ec.CipherSuite),
|
||||
BlockSize: ec.BlockSize,
|
||||
}
|
||||
}
|
||||
|
||||
// ListOptions options for listing objects
|
||||
type ListOptions struct {
|
||||
Prefix string
|
||||
Cursor string // Cursor is relative to Prefix, full path is Prefix + Cursor
|
||||
Delimiter int32
|
||||
Recursive bool
|
||||
Direction int
|
||||
Limit int
|
||||
}
|
||||
|
||||
// ListObjects list objects in bucket, if authorized.
|
||||
func (bucket *Bucket) ListObjects(options *ListOptions) (*ObjectList, error) {
|
||||
scope := bucket.scope.child()
|
||||
|
||||
opts := &storj.ListOptions{}
|
||||
if options != nil {
|
||||
opts.Prefix = options.Prefix
|
||||
opts.Cursor = options.Cursor
|
||||
opts.Direction = storj.ListDirection(options.Direction)
|
||||
opts.Delimiter = options.Delimiter
|
||||
opts.Recursive = options.Recursive
|
||||
opts.Limit = options.Limit
|
||||
}
|
||||
|
||||
list, err := bucket.lib.ListObjects(scope.ctx, opts)
|
||||
if err != nil {
|
||||
return nil, safeError(err)
|
||||
}
|
||||
return &ObjectList{list}, nil
|
||||
}
|
||||
|
||||
// OpenObject returns an Object handle, if authorized.
|
||||
func (bucket *Bucket) OpenObject(objectPath string) (*ObjectInfo, error) {
|
||||
scope := bucket.scope.child()
|
||||
object, err := bucket.lib.OpenObject(scope.ctx, objectPath)
|
||||
if err != nil {
|
||||
return nil, safeError(err)
|
||||
}
|
||||
return newObjectInfoFromObjectMeta(object.Meta), nil
|
||||
}
|
||||
|
||||
// DeleteObject removes an object, if authorized.
|
||||
func (bucket *Bucket) DeleteObject(objectPath string) error {
|
||||
scope := bucket.scope.child()
|
||||
return safeError(bucket.lib.DeleteObject(scope.ctx, objectPath))
|
||||
}
|
||||
|
||||
// Close closes the Bucket session.
|
||||
func (bucket *Bucket) Close() error {
|
||||
defer bucket.cancel()
|
||||
return safeError(bucket.lib.Close())
|
||||
}
|
||||
|
||||
// WriterOptions controls options about writing a new Object
|
||||
type WriterOptions struct {
|
||||
// ContentType, if set, gives a MIME content-type for the Object.
|
||||
ContentType string
|
||||
// Metadata contains additional information about an Object. It can
|
||||
// hold arbitrary textual fields and can be retrieved together with the
|
||||
// Object. Field names can be at most 1024 bytes long. Field values are
|
||||
// not individually limited in size, but the total of all metadata
|
||||
// (fields and values) can not exceed 4 kiB.
|
||||
Metadata map[string]string
|
||||
// Expires is the time at which the new Object can expire (be deleted
|
||||
// automatically from storage nodes).
|
||||
Expires int
|
||||
|
||||
// EncryptionParameters determines the cipher suite to use for
|
||||
// the Object's data encryption. If not set, the Bucket's
|
||||
// defaults will be used.
|
||||
EncryptionParameters *EncryptionParameters
|
||||
|
||||
// RedundancyScheme determines the Reed-Solomon and/or Forward
|
||||
// Error Correction encoding parameters to be used for this
|
||||
// Object.
|
||||
RedundancyScheme *RedundancyScheme
|
||||
}
|
||||
|
||||
// NewWriterOptions creates writer options
|
||||
func NewWriterOptions() *WriterOptions {
|
||||
return &WriterOptions{}
|
||||
}
|
||||
|
||||
// Writer writes data into object
|
||||
type Writer struct {
|
||||
scope
|
||||
writer io.WriteCloser
|
||||
}
|
||||
|
||||
// NewWriter creates instance of Writer
|
||||
func (bucket *Bucket) NewWriter(path storj.Path, options *WriterOptions) (*Writer, error) {
|
||||
scope := bucket.scope.child()
|
||||
|
||||
opts := &libuplink.UploadOptions{}
|
||||
if options != nil {
|
||||
opts.ContentType = options.ContentType
|
||||
opts.Metadata = options.Metadata
|
||||
if options.Expires != 0 {
|
||||
opts.Expires = time.Unix(int64(options.Expires), 0)
|
||||
}
|
||||
opts.Volatile.EncryptionParameters = newStorjEncryptionParameters(options.EncryptionParameters)
|
||||
opts.Volatile.RedundancyScheme = newStorjRedundancyScheme(options.RedundancyScheme)
|
||||
}
|
||||
|
||||
writer, err := bucket.lib.NewWriter(scope.ctx, path, opts)
|
||||
if err != nil {
|
||||
return nil, safeError(err)
|
||||
}
|
||||
return &Writer{scope, writer}, nil
|
||||
}
|
||||
|
||||
// Write writes data.length bytes from data to the underlying data stream.
|
||||
func (w *Writer) Write(data []byte, offset, length int32) (int32, error) {
|
||||
// in Java byte array size is max int32
|
||||
n, err := w.writer.Write(data[offset:length])
|
||||
return int32(n), safeError(err)
|
||||
}
|
||||
|
||||
// Cancel cancels writing operation
|
||||
func (w *Writer) Cancel() {
|
||||
w.cancel()
|
||||
}
|
||||
|
||||
// Close closes writer
|
||||
func (w *Writer) Close() error {
|
||||
defer w.cancel()
|
||||
return safeError(w.writer.Close())
|
||||
}
|
||||
|
||||
// ReaderOptions options for reading
|
||||
type ReaderOptions struct {
|
||||
}
|
||||
|
||||
// Reader reader for downloading object
|
||||
type Reader struct {
|
||||
scope
|
||||
readError error
|
||||
reader interface {
|
||||
io.Reader
|
||||
io.Seeker
|
||||
io.Closer
|
||||
}
|
||||
}
|
||||
|
||||
// NewReader returns new reader for downloading object
|
||||
func (bucket *Bucket) NewReader(path storj.Path, options *ReaderOptions) (*Reader, error) {
|
||||
scope := bucket.scope.child()
|
||||
|
||||
reader, err := bucket.lib.NewReader(scope.ctx, path)
|
||||
if err != nil {
|
||||
return nil, safeError(err)
|
||||
}
|
||||
return &Reader{
|
||||
scope: scope,
|
||||
reader: reader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Read reads data into byte array
|
||||
func (r *Reader) Read(data []byte) (n int32, err error) {
|
||||
if r.readError != nil {
|
||||
err = r.readError
|
||||
} else {
|
||||
var read int
|
||||
read, err = r.reader.Read(data)
|
||||
n = int32(read)
|
||||
}
|
||||
|
||||
if n > 0 && err != nil {
|
||||
r.readError = err
|
||||
err = nil
|
||||
}
|
||||
|
||||
if err == io.EOF {
|
||||
return -1, nil
|
||||
}
|
||||
return n, safeError(err)
|
||||
}
|
||||
|
||||
// Cancel cancels read operation
|
||||
func (r *Reader) Cancel() {
|
||||
r.cancel()
|
||||
}
|
||||
|
||||
// Close closes reader
|
||||
func (r *Reader) Close() error {
|
||||
defer r.cancel()
|
||||
return safeError(r.reader.Close())
|
||||
}
|
53
mobile/build.sh
Executable file
@ -0,0 +1,53 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This script will build libuplink-android.aar library from scratch
|
||||
# Required:
|
||||
# * ANDROID_HOME set with NDK available
|
||||
# * go
|
||||
# * gospace
|
||||
|
||||
if [ -z "$ANDROID_HOME" ]
|
||||
then
|
||||
echo "\$ANDROID_HOME is not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
OUTPUT_DIR=${1:-$PWD}
|
||||
OUTPUT_AAR="libuplink-android.aar"
|
||||
OUTPUT_JAVA_PACKAGE="io.storj.libuplink"
|
||||
|
||||
STORJ_PATH=~/storj-for-android
|
||||
|
||||
# set go modules to default behavior
|
||||
export GO111MODULE=auto
|
||||
|
||||
# go knows where our gopath is
|
||||
export GOPATH=$STORJ_PATH
|
||||
|
||||
# gospace knows where our gopath is (this is to avoid accidental damage to existing GOPATH)
|
||||
# you should not use default GOPATH here
|
||||
export GOSPACE_ROOT=$STORJ_PATH
|
||||
|
||||
# set the github repository that this GOSPACE manages
|
||||
export GOSPACE_PKG=storj.io/storj
|
||||
|
||||
# set the where the repository is located
|
||||
export GOSPACE_REPO=git@github.com:storj/storj.git
|
||||
|
||||
gospace setup
|
||||
|
||||
export PATH=$PATH:$GOPATH/bin
|
||||
|
||||
# step can be removed after merge to master
|
||||
cd $GOPATH/src/storj.io/storj
|
||||
git checkout -q mn/java-bindings
|
||||
|
||||
cd $GOPATH
|
||||
|
||||
go get golang.org/x/mobile/cmd/gomobile
|
||||
|
||||
gomobile init
|
||||
|
||||
echo -e "\nbuilding aar"
|
||||
gomobile bind -target android -o $OUTPUT_DIR/libuplink-android.aar -javapkg $OUTPUT_JAVA_PACKAGE storj.io/storj/mobile
|
||||
echo "output aar: $OUTPUT_DIR/libuplink-android.aar"
|
25
mobile/doc.go
Normal file
@ -0,0 +1,25 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
// Package mobile contains the simplified mobile APIs to Storj Network.
|
||||
//
|
||||
// For API limitations see https://github.com/ethereum/go-ethereum/blob/461291882edce0ac4a28f64c4e8725b7f57cbeae/mobile/doc.go#L23
|
||||
//
|
||||
//
|
||||
// build loop for development
|
||||
// watchrun gobind -lang=java -outdir=../mobile-out storj.io/storj/mobile == pt skipped ../mobile-out
|
||||
//
|
||||
// gomobile bind -target android
|
||||
//
|
||||
// To use:
|
||||
// gomobile bind -target android
|
||||
//
|
||||
// Create a new project in AndroidStudio
|
||||
//
|
||||
// Copy mobile-source.jar and mobile.aar into `AndroidStudioProjects\MyApplication\app\libs\`
|
||||
//
|
||||
// Modify build.gradle to also find *.aar files:
|
||||
// implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
|
||||
//
|
||||
// See example Java file
|
||||
package mobile
|
13
mobile/libuplink_android/.gitignore
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
1
mobile/libuplink_android/app/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/build
|
27
mobile/libuplink_android/app/build.gradle
Normal file
@ -0,0 +1,27 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
defaultConfig {
|
||||
applicationId "io.storj.mobile.libuplink"
|
||||
minSdkVersion 15
|
||||
targetSdkVersion 28
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
|
||||
implementation 'com.android.support:appcompat-v7:28.0.0'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
||||
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
|
||||
}
|
21
mobile/libuplink_android/app/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
@ -0,0 +1,338 @@
|
||||
package io.storj.mobile.libuplink;
|
||||
|
||||
import android.support.test.InstrumentationRegistry;
|
||||
import android.support.test.runner.AndroidJUnit4;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.HashSet;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
|
||||
import io.storj.libuplink.mobile.*;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class LibuplinkInstrumentedTest {
|
||||
|
||||
public static final String VALID_SATELLITE_ADDRESS = InstrumentationRegistry.getArguments().getString("storj.sim.host", "192.168.8.134:10000");
|
||||
public static final String VALID_API_KEY = InstrumentationRegistry.getArguments().getString("api.key", "GBK6TEMIPJQUOVVN99C2QO9USKTU26QB6C4VNM0=");
|
||||
|
||||
String filesDir;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
filesDir = InstrumentationRegistry.getTargetContext().getFilesDir().getAbsolutePath();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOpenProjectFail() throws Exception {
|
||||
Config config = new Config();
|
||||
|
||||
Uplink uplink = new Uplink(config, filesDir);
|
||||
try {
|
||||
ProjectOptions options = new ProjectOptions();
|
||||
options.setEncryptionKey("TestEncryptionKey".getBytes());
|
||||
|
||||
Project project = null;
|
||||
try {
|
||||
// 10.0.2.2 refers to not existing satellite
|
||||
project = uplink.openProject("10.0.2.2:1", VALID_API_KEY, options);
|
||||
fail("exception expected");
|
||||
} catch (Exception e) {
|
||||
// skip
|
||||
} finally {
|
||||
if (project != null) {
|
||||
project.close();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
uplink.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBasicBucket() throws Exception {
|
||||
Config config = new Config();
|
||||
|
||||
Uplink uplink = new Uplink(config, filesDir);
|
||||
try {
|
||||
ProjectOptions options = new ProjectOptions();
|
||||
options.setEncryptionKey("TestEncryptionKey".getBytes());
|
||||
|
||||
Project project = null;
|
||||
try {
|
||||
project = uplink.openProject(VALID_SATELLITE_ADDRESS, VALID_API_KEY, options);
|
||||
|
||||
String expectedBucket = "testBucket";
|
||||
project.createBucket(expectedBucket, new BucketConfig());
|
||||
BucketInfo bucketInfo = project.getBucketInfo(expectedBucket);
|
||||
Assert.assertEquals(expectedBucket, bucketInfo.getName());
|
||||
|
||||
project.deleteBucket(expectedBucket);
|
||||
|
||||
try {
|
||||
project.getBucketInfo(expectedBucket);
|
||||
} catch (Exception e) {
|
||||
Assert.assertTrue(e.getMessage().startsWith("bucket not found"));
|
||||
}
|
||||
} finally {
|
||||
if (project != null) {
|
||||
project.close();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
uplink.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testListBuckets() throws Exception {
|
||||
Config config = new Config();
|
||||
|
||||
Uplink uplink = new Uplink(config, filesDir);
|
||||
try {
|
||||
ProjectOptions options = new ProjectOptions();
|
||||
options.setEncryptionKey("TestEncryptionKey".getBytes());
|
||||
|
||||
Project project = uplink.openProject(VALID_SATELLITE_ADDRESS, VALID_API_KEY, options);
|
||||
try {
|
||||
BucketConfig bucketConfig = new BucketConfig();
|
||||
Set<String> expectedBuckets = new HashSet<>();
|
||||
for (int i = 0; i < 10; i++) {
|
||||
String expectedBucket = "testBucket" + i;
|
||||
project.createBucket(expectedBucket, bucketConfig);
|
||||
expectedBuckets.add(expectedBucket);
|
||||
}
|
||||
|
||||
BucketList bucketList = project.listBuckets("", 1, 100);
|
||||
assertEquals(false, bucketList.more());
|
||||
String aa = "";
|
||||
for (int i = 0; i < bucketList.length(); i++) {
|
||||
aa += bucketList.item(i).getName() + "|";
|
||||
}
|
||||
|
||||
assertEquals(aa, expectedBuckets.size(), bucketList.length());
|
||||
|
||||
for (String bucket : expectedBuckets) {
|
||||
project.deleteBucket(bucket);
|
||||
}
|
||||
|
||||
bucketList = project.listBuckets("", 1, 100);
|
||||
assertEquals(false, bucketList.more());
|
||||
assertEquals(0, bucketList.length());
|
||||
} finally {
|
||||
project.close();
|
||||
}
|
||||
} finally {
|
||||
uplink.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUploadDownloadInline() throws Exception {
|
||||
Config config = new Config();
|
||||
|
||||
Uplink uplink = new Uplink(config, filesDir);
|
||||
try {
|
||||
ProjectOptions options = new ProjectOptions();
|
||||
options.setEncryptionKey("TestEncryptionKey".getBytes());
|
||||
|
||||
Project project = uplink.openProject(VALID_SATELLITE_ADDRESS, VALID_API_KEY, options);
|
||||
try {
|
||||
BucketAccess access = new BucketAccess();
|
||||
access.setPathEncryptionKey("TestEncryptionKey".getBytes());
|
||||
|
||||
RedundancyScheme scheme = new RedundancyScheme();
|
||||
scheme.setRequiredShares((short) 2);
|
||||
scheme.setRepairShares((short) 4);
|
||||
scheme.setOptimalShares((short) 6);
|
||||
scheme.setTotalShares((short) 8);
|
||||
|
||||
BucketConfig bucketConfig = new BucketConfig();
|
||||
bucketConfig.setRedundancyScheme(scheme);
|
||||
|
||||
project.createBucket("test", bucketConfig);
|
||||
|
||||
Bucket bucket = project.openBucket("test", access);
|
||||
|
||||
byte[] expectedData = new byte[1024];
|
||||
Random random = new Random();
|
||||
random.nextBytes(expectedData);
|
||||
|
||||
{
|
||||
Writer writer = bucket.newWriter("object/path", new WriterOptions());
|
||||
try {
|
||||
writer.write(expectedData,0, expectedData.length);
|
||||
} finally {
|
||||
writer.close();
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
Reader reader = bucket.newReader("object/path", new ReaderOptions());
|
||||
try {
|
||||
ByteArrayOutputStream writer = new ByteArrayOutputStream();
|
||||
byte[] buf = new byte[256];
|
||||
int read = 0;
|
||||
while ((read = reader.read(buf)) != -1) {
|
||||
writer.write(buf, 0, read);
|
||||
}
|
||||
assertArrayEquals(writer.toByteArray(), expectedData);
|
||||
} finally {
|
||||
reader.close();
|
||||
}
|
||||
}
|
||||
|
||||
bucket.close();
|
||||
|
||||
project.deleteBucket("test");
|
||||
} finally {
|
||||
project.close();
|
||||
}
|
||||
} finally {
|
||||
uplink.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testUploadDownloadDeleteRemote() throws Exception {
|
||||
Config config = new Config();
|
||||
|
||||
Uplink uplink = new Uplink(config, filesDir);
|
||||
try {
|
||||
ProjectOptions options = new ProjectOptions();
|
||||
options.setEncryptionKey("TestEncryptionKey".getBytes());
|
||||
|
||||
Project project = uplink.openProject(VALID_SATELLITE_ADDRESS, VALID_API_KEY, options);
|
||||
try {
|
||||
BucketAccess access = new BucketAccess();
|
||||
access.setPathEncryptionKey("TestEncryptionKey".getBytes());
|
||||
|
||||
RedundancyScheme scheme = new RedundancyScheme();
|
||||
scheme.setRequiredShares((short) 2);
|
||||
scheme.setRepairShares((short) 4);
|
||||
scheme.setOptimalShares((short) 6);
|
||||
scheme.setTotalShares((short) 8);
|
||||
|
||||
BucketConfig bucketConfig = new BucketConfig();
|
||||
bucketConfig.setRedundancyScheme(scheme);
|
||||
|
||||
project.createBucket("test", bucketConfig);
|
||||
|
||||
Bucket bucket = project.openBucket("test", access);
|
||||
|
||||
byte[] expectedData = new byte[1024 * 100];
|
||||
Random random = new Random();
|
||||
random.nextBytes(expectedData);
|
||||
{
|
||||
Writer writer = bucket.newWriter("object/path", new WriterOptions());
|
||||
try {
|
||||
writer.write(expectedData, 0, expectedData.length);
|
||||
} finally {
|
||||
writer.close();
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
Reader reader = bucket.newReader("object/path", new ReaderOptions());
|
||||
try {
|
||||
ByteArrayOutputStream writer = new ByteArrayOutputStream();
|
||||
byte[] buf = new byte[4096];
|
||||
int read = 0;
|
||||
while ((read = reader.read(buf)) != -1) {
|
||||
writer.write(buf, 0, read);
|
||||
}
|
||||
assertEquals(expectedData.length, writer.size());
|
||||
} finally {
|
||||
reader.close();
|
||||
}
|
||||
}
|
||||
|
||||
bucket.deleteObject("object/path");
|
||||
|
||||
try {
|
||||
bucket.deleteObject("object/path");
|
||||
} catch (Exception e) {
|
||||
assertTrue(e.getMessage().startsWith("object not found"));
|
||||
}
|
||||
|
||||
bucket.close();
|
||||
|
||||
project.deleteBucket("test");
|
||||
} finally {
|
||||
project.close();
|
||||
}
|
||||
} finally {
|
||||
uplink.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testListObjects() throws Exception {
|
||||
Config config = new Config();
|
||||
|
||||
Uplink uplink = new Uplink(config, filesDir);
|
||||
try {
|
||||
ProjectOptions options = new ProjectOptions();
|
||||
options.setEncryptionKey("TestEncryptionKey".getBytes());
|
||||
|
||||
Project project = uplink.openProject(VALID_SATELLITE_ADDRESS, VALID_API_KEY, options);
|
||||
try {
|
||||
BucketAccess access = new BucketAccess();
|
||||
access.setPathEncryptionKey("TestEncryptionKey".getBytes());
|
||||
|
||||
BucketConfig bucketConfig = new BucketConfig();
|
||||
bucketConfig.setRedundancyScheme(new RedundancyScheme());
|
||||
|
||||
BucketInfo bucketInfo = project.createBucket("testBucket", bucketConfig);
|
||||
assertEquals("testBucket", bucketInfo.getName());
|
||||
|
||||
Bucket bucket = project.openBucket("testBucket", access);
|
||||
|
||||
long before = System.currentTimeMillis();
|
||||
|
||||
for (int i = 0; i < 13; i++) {
|
||||
Writer writer = bucket.newWriter("path" + i, new WriterOptions());
|
||||
try {
|
||||
byte[] buf = new byte[0];
|
||||
writer.write(buf, 0, buf.length);
|
||||
} finally {
|
||||
writer.close();
|
||||
}
|
||||
}
|
||||
|
||||
ListOptions listOptions = new ListOptions();
|
||||
listOptions.setCursor("");
|
||||
listOptions.setDirection(Mobile.DirectionForward);
|
||||
listOptions.setLimit(20);
|
||||
|
||||
ObjectList list = bucket.listObjects(listOptions);
|
||||
assertEquals(13, list.length());
|
||||
|
||||
for (int i = 0; i < list.length(); i++) {
|
||||
ObjectInfo info = list.item(i);
|
||||
assertEquals("testBucket", info.getBucket());
|
||||
assertTrue(info.getCreated() >= before);
|
||||
|
||||
// cleanup
|
||||
bucket.deleteObject("path" + i);
|
||||
}
|
||||
|
||||
bucket.close();
|
||||
|
||||
project.deleteBucket("testBucket");
|
||||
} finally {
|
||||
project.close();
|
||||
}
|
||||
} finally {
|
||||
uplink.close();
|
||||
}
|
||||
}
|
||||
}
|
14
mobile/libuplink_android/app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,14 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="io.storj.mobile.libuplink">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme" />
|
||||
</manifest>
|
@ -0,0 +1,34 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="78.5885"
|
||||
android:endY="90.9159"
|
||||
android:startX="48.7653"
|
||||
android:startY="61.0927"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#008577"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 4.8 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 4.4 KiB |
After Width: | Height: | Size: 6.7 KiB |
After Width: | Height: | Size: 6.2 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 8.9 KiB |
After Width: | Height: | Size: 15 KiB |
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="colorPrimary">#008577</color>
|
||||
<color name="colorPrimaryDark">#00574B</color>
|
||||
<color name="colorAccent">#D81B60</color>
|
||||
</resources>
|
@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">libuplink_android</string>
|
||||
</resources>
|
11
mobile/libuplink_android/app/src/main/res/values/styles.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<resources>
|
||||
|
||||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="colorAccent">@color/colorAccent</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
27
mobile/libuplink_android/build.gradle
Normal file
@ -0,0 +1,27 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.4.0'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
15
mobile/libuplink_android/gradle.properties
Normal file
@ -0,0 +1,15 @@
|
||||
# Project-wide Gradle settings.
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx1536m
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
|
||||
|
BIN
mobile/libuplink_android/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
6
mobile/libuplink_android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
#Tue May 07 14:29:06 CEST 2019
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip
|
172
mobile/libuplink_android/gradlew
vendored
Executable file
@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS=""
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=$((i+1))
|
||||
done
|
||||
case $i in
|
||||
(0) set -- ;;
|
||||
(1) set -- "$args0" ;;
|
||||
(2) set -- "$args0" "$args1" ;;
|
||||
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=$(save "$@")
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
||||
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
|
||||
cd "$(dirname "$0")"
|
||||
fi
|
||||
|
||||
exec "$JAVACMD" "$@"
|
84
mobile/libuplink_android/gradlew.bat
vendored
Normal file
@ -0,0 +1,84 @@
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS=
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windows variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
1
mobile/libuplink_android/settings.gradle
Normal file
@ -0,0 +1 @@
|
||||
include ':app'
|
96
mobile/object.go
Normal file
@ -0,0 +1,96 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
package mobile
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
libuplink "storj.io/storj/lib/uplink"
|
||||
"storj.io/storj/pkg/storj"
|
||||
)
|
||||
|
||||
// ObjectInfo object metadata
|
||||
type ObjectInfo struct {
|
||||
Version int32
|
||||
Bucket string
|
||||
Path string
|
||||
IsPrefix bool
|
||||
Size int64
|
||||
ContentType string
|
||||
Created int64
|
||||
Modified int64
|
||||
Expires int64
|
||||
|
||||
metadata map[string]string
|
||||
}
|
||||
|
||||
func newObjectInfoFromObject(object storj.Object) *ObjectInfo {
|
||||
return &ObjectInfo{
|
||||
Version: int32(object.Version),
|
||||
Bucket: object.Bucket.Name,
|
||||
Path: object.Path,
|
||||
IsPrefix: object.IsPrefix,
|
||||
Size: object.Size,
|
||||
ContentType: object.ContentType,
|
||||
Created: object.Created.UTC().UnixNano() / int64(time.Millisecond),
|
||||
Modified: object.Modified.UTC().UnixNano() / int64(time.Millisecond),
|
||||
Expires: object.Expires.UTC().UnixNano() / int64(time.Millisecond),
|
||||
metadata: object.Metadata,
|
||||
}
|
||||
}
|
||||
|
||||
func newObjectInfoFromObjectMeta(objectMeta libuplink.ObjectMeta) *ObjectInfo {
|
||||
return &ObjectInfo{
|
||||
// TODO ObjectMeta doesn't have Version but storj.Object has
|
||||
// Version: int32(objectMeta.Version),
|
||||
Bucket: objectMeta.Bucket,
|
||||
Path: objectMeta.Path,
|
||||
IsPrefix: objectMeta.IsPrefix,
|
||||
Size: objectMeta.Size,
|
||||
ContentType: objectMeta.ContentType,
|
||||
Created: objectMeta.Created.UTC().UnixNano() / int64(time.Millisecond),
|
||||
Modified: objectMeta.Modified.UTC().UnixNano() / int64(time.Millisecond),
|
||||
Expires: objectMeta.Expires.UTC().UnixNano() / int64(time.Millisecond),
|
||||
metadata: objectMeta.Metadata,
|
||||
}
|
||||
}
|
||||
|
||||
// GetMetadata gets objects custom metadata
|
||||
func (bl *ObjectInfo) GetMetadata(key string) string {
|
||||
return bl.metadata[key]
|
||||
}
|
||||
|
||||
// ObjectList represents list of objects
|
||||
type ObjectList struct {
|
||||
list storj.ObjectList
|
||||
}
|
||||
|
||||
// More returns true if list request was not able to return all results
|
||||
func (bl *ObjectList) More() bool {
|
||||
return bl.list.More
|
||||
}
|
||||
|
||||
// Prefix prefix for objects from list
|
||||
func (bl *ObjectList) Prefix() string {
|
||||
return bl.list.Prefix
|
||||
}
|
||||
|
||||
// Bucket returns bucket name
|
||||
func (bl *ObjectList) Bucket() string {
|
||||
return bl.list.Bucket
|
||||
}
|
||||
|
||||
// Length returns number of returned items
|
||||
func (bl *ObjectList) Length() int {
|
||||
return len(bl.list.Items)
|
||||
}
|
||||
|
||||
// Item gets item from specific index
|
||||
func (bl *ObjectList) Item(index int) (*ObjectInfo, error) {
|
||||
if index < 0 && index >= len(bl.list.Items) {
|
||||
return nil, fmt.Errorf("index out of range")
|
||||
}
|
||||
return newObjectInfoFromObject(bl.list.Items[index]), nil
|
||||
}
|
31
mobile/scope.go
Normal file
@ -0,0 +1,31 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
package mobile
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"storj.io/storj/internal/fpath"
|
||||
)
|
||||
|
||||
type scope struct {
|
||||
ctx context.Context
|
||||
cancel func()
|
||||
}
|
||||
|
||||
func rootScope(tempDir string) scope {
|
||||
ctx := context.Background()
|
||||
if tempDir == "inmemory" {
|
||||
ctx = fpath.WithTempData(ctx, "", true)
|
||||
} else {
|
||||
ctx = fpath.WithTempData(ctx, tempDir, false)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
return scope{ctx, cancel}
|
||||
}
|
||||
|
||||
func (parent *scope) child() scope {
|
||||
ctx, cancel := context.WithCancel(parent.ctx)
|
||||
return scope{ctx, cancel}
|
||||
}
|
11
mobile/test-libuplink-android.sh
Executable file
@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
set -ueo pipefail
|
||||
|
||||
echo "Executing gomobile bind"
|
||||
|
||||
gomobile bind -target android -o libuplink_android/app/libs/libuplink-android.aar -javapkg io.storj.libuplink storj.io/storj/mobile
|
||||
|
||||
cd libuplink_android
|
||||
|
||||
# Might be easier way than -Pandroid.testInstrumentationRunnerArguments
|
||||
./gradlew connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.api.key=$GATEWAY_0_API_KEY -Pandroid.testInstrumentationRunnerArguments.storj.sim.host=$SATELLITE_0_ADDR
|
21
mobile/test-sim.sh
Executable file
@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
set -ueo pipefail
|
||||
set +x
|
||||
|
||||
# setup tmpdir for testfiles and cleanup
|
||||
TMP=$(mktemp -d -t tmp.XXXXXXXXXX)
|
||||
cleanup(){
|
||||
rm -rf "$TMP"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
export STORJ_NETWORK_DIR=$TMP
|
||||
|
||||
STORJ_NETWORK_HOST4=${STORJ_NETWORK_HOST4:-127.0.0.1}
|
||||
|
||||
# setup the network
|
||||
storj-sim -x --host $STORJ_NETWORK_HOST4 network setup
|
||||
|
||||
# run tests
|
||||
storj-sim -x --host $STORJ_NETWORK_HOST4 network test bash test-libuplink-android.sh
|
||||
storj-sim -x --host $STORJ_NETWORK_HOST4 network destroy
|
191
mobile/uplink.go
Normal file
@ -0,0 +1,191 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
package mobile
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"storj.io/storj/internal/memory"
|
||||
libuplink "storj.io/storj/lib/uplink"
|
||||
"storj.io/storj/pkg/storj"
|
||||
)
|
||||
|
||||
// Config represents configuration options for an Uplink
|
||||
type Config struct {
|
||||
|
||||
// MaxInlineSize determines whether the uplink will attempt to
|
||||
// store a new object in the satellite's metainfo. Objects at
|
||||
// or below this size will be marked for inline storage, and
|
||||
// objects above this size will not. (The satellite may reject
|
||||
// the inline storage and require remote storage, still.)
|
||||
MaxInlineSize int64
|
||||
|
||||
// MaxMemory is the default maximum amount of memory to be
|
||||
// allocated for read buffers while performing decodes of
|
||||
// objects. (This option is overrideable per Bucket if the user
|
||||
// so desires.) If set to zero, the library default (4 MiB) will
|
||||
// be used. If set to a negative value, the system will use the
|
||||
// smallest amount of memory it can.
|
||||
MaxMemory int64
|
||||
}
|
||||
|
||||
// Uplink represents the main entrypoint to Storj V3. An Uplink connects to
|
||||
// a specific Satellite and caches connections and resources, allowing one to
|
||||
// create sessions delineated by specific access controls.
|
||||
type Uplink struct {
|
||||
scope
|
||||
lib *libuplink.Uplink
|
||||
}
|
||||
|
||||
// NewUplink creates a new Uplink. This is the first step to create an uplink
|
||||
// session with a user specified config or with default config, if nil config.
|
||||
// Uplink needs also writable temporary directory.
|
||||
func NewUplink(config *Config, tempDir string) (*Uplink, error) {
|
||||
scope := rootScope(tempDir)
|
||||
|
||||
cfg := &libuplink.Config{}
|
||||
if config != nil {
|
||||
cfg.Volatile.TLS.SkipPeerCAWhitelist = true
|
||||
cfg.Volatile.MaxInlineSize = memory.Size(config.MaxInlineSize)
|
||||
cfg.Volatile.MaxMemory = memory.Size(config.MaxMemory)
|
||||
}
|
||||
|
||||
lib, err := libuplink.NewUplink(scope.ctx, cfg)
|
||||
if err != nil {
|
||||
return nil, safeError(err)
|
||||
}
|
||||
return &Uplink{scope, lib}, nil
|
||||
}
|
||||
|
||||
// Close closes the Uplink. This may not do anything at present, but should
|
||||
// still be called to allow forward compatibility. No Project or Bucket
|
||||
// objects using this Uplink should be used after calling Close.
|
||||
func (uplink *Uplink) Close() error {
|
||||
uplink.cancel()
|
||||
return safeError(uplink.lib.Close())
|
||||
}
|
||||
|
||||
// ProjectOptions allows configuration of various project options during opening
|
||||
type ProjectOptions struct {
|
||||
EncryptionKey []byte
|
||||
}
|
||||
|
||||
// Project represents a specific project access session.
|
||||
type Project struct {
|
||||
scope
|
||||
lib *libuplink.Project
|
||||
}
|
||||
|
||||
// OpenProject returns a Project handle with the given APIKey
|
||||
func (uplink *Uplink) OpenProject(satellite string, apikey string, options *ProjectOptions) (*Project, error) {
|
||||
scope := uplink.scope.child()
|
||||
|
||||
opts := libuplink.ProjectOptions{}
|
||||
if options != nil {
|
||||
opts.Volatile.EncryptionKey = &storj.Key{}
|
||||
copy(opts.Volatile.EncryptionKey[:], options.EncryptionKey) // TODO: error check
|
||||
}
|
||||
|
||||
key, err := libuplink.ParseAPIKey(apikey)
|
||||
if err != nil {
|
||||
return nil, safeError(err)
|
||||
}
|
||||
|
||||
project, err := uplink.lib.OpenProject(scope.ctx, satellite, key, &opts)
|
||||
if err != nil {
|
||||
return nil, safeError(err)
|
||||
}
|
||||
|
||||
return &Project{scope, project}, nil
|
||||
}
|
||||
|
||||
// Close closes the Project
|
||||
func (project *Project) Close() error {
|
||||
defer project.cancel()
|
||||
return safeError(project.lib.Close())
|
||||
}
|
||||
|
||||
// CreateBucket creates buckets in project
|
||||
func (project *Project) CreateBucket(bucketName string, opts *BucketConfig) (*BucketInfo, error) {
|
||||
scope := project.scope.child()
|
||||
|
||||
cfg := libuplink.BucketConfig{}
|
||||
if opts != nil {
|
||||
cfg.PathCipher = storj.CipherSuite(opts.PathCipher)
|
||||
cfg.EncryptionParameters = newStorjEncryptionParameters(opts.EncryptionParameters)
|
||||
cfg.Volatile.RedundancyScheme = newStorjRedundancyScheme(opts.RedundancyScheme)
|
||||
cfg.Volatile.SegmentsSize = memory.Size(opts.SegmentsSize)
|
||||
}
|
||||
|
||||
bucket, err := project.lib.CreateBucket(scope.ctx, bucketName, &cfg)
|
||||
if err != nil {
|
||||
return nil, safeError(err)
|
||||
}
|
||||
|
||||
return newBucketInfo(bucket), nil
|
||||
}
|
||||
|
||||
// OpenBucket returns a Bucket handle with the given EncryptionAccess
|
||||
// information.
|
||||
func (project *Project) OpenBucket(bucketName string, options *BucketAccess) (*Bucket, error) {
|
||||
scope := project.scope.child()
|
||||
|
||||
opts := libuplink.EncryptionAccess{}
|
||||
if options != nil {
|
||||
copy(opts.Key[:], options.PathEncryptionKey) // TODO: error check
|
||||
opts.EncryptedPathPrefix = options.EncryptedPathPrefix
|
||||
}
|
||||
|
||||
bucket, err := project.lib.OpenBucket(scope.ctx, bucketName, &opts)
|
||||
if err != nil {
|
||||
return nil, safeError(err)
|
||||
}
|
||||
|
||||
return &Bucket{bucket.Name, scope, bucket}, nil
|
||||
}
|
||||
|
||||
// GetBucketInfo returns info about the requested bucket if authorized.
|
||||
func (project *Project) GetBucketInfo(bucketName string) (*BucketInfo, error) {
|
||||
scope := project.scope.child()
|
||||
|
||||
bucket, _, err := project.lib.GetBucketInfo(scope.ctx, bucketName)
|
||||
if err != nil {
|
||||
return nil, safeError(err)
|
||||
}
|
||||
|
||||
return newBucketInfo(bucket), nil
|
||||
}
|
||||
|
||||
// ListBuckets will list authorized buckets.
|
||||
func (project *Project) ListBuckets(cursor string, direction, limit int) (*BucketList, error) {
|
||||
scope := project.scope.child()
|
||||
opts := libuplink.BucketListOptions{
|
||||
Cursor: cursor,
|
||||
Direction: storj.ListDirection(direction),
|
||||
Limit: limit,
|
||||
}
|
||||
list, err := project.lib.ListBuckets(scope.ctx, &opts)
|
||||
if err != nil {
|
||||
return nil, safeError(err)
|
||||
}
|
||||
|
||||
return &BucketList{list}, nil
|
||||
}
|
||||
|
||||
// DeleteBucket deletes a bucket if authorized. If the bucket contains any
|
||||
// Objects at the time of deletion, they may be lost permanently.
|
||||
func (project *Project) DeleteBucket(bucketName string) error {
|
||||
scope := project.scope.child()
|
||||
|
||||
err := project.lib.DeleteBucket(scope.ctx, bucketName)
|
||||
return safeError(err)
|
||||
}
|
||||
|
||||
func safeError(err error) error {
|
||||
// workaround to avoid gomobile panic because of "hash of unhashable type errs.combinedError"
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("%v", err.Error())
|
||||
}
|
@ -12,6 +12,8 @@ import (
|
||||
"github.com/vivint/infectious"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"storj.io/storj/internal/fpath"
|
||||
"storj.io/storj/internal/memory"
|
||||
"storj.io/storj/internal/readcloser"
|
||||
"storj.io/storj/internal/sync2"
|
||||
"storj.io/storj/pkg/encryption"
|
||||
@ -123,13 +125,25 @@ type encodedReader struct {
|
||||
|
||||
// EncodeReader takes a Reader and a RedundancyStrategy and returns a slice of
|
||||
// io.ReadClosers.
|
||||
func EncodeReader(ctx context.Context, r io.Reader, rs RedundancyStrategy) ([]io.ReadCloser, error) {
|
||||
func EncodeReader(ctx context.Context, r io.Reader, rs RedundancyStrategy) (_ []io.ReadCloser, err error) {
|
||||
er := &encodedReader{
|
||||
rs: rs,
|
||||
pieces: make(map[int]*encodedPiece, rs.TotalCount()),
|
||||
}
|
||||
|
||||
pipeReaders, pipeWriter, err := sync2.NewTeeFile(rs.TotalCount(), os.TempDir())
|
||||
var pipeReaders []sync2.PipeReader
|
||||
var pipeWriter sync2.PipeWriter
|
||||
|
||||
tempDir, inmemory, _ := fpath.GetTempData(ctx)
|
||||
if inmemory {
|
||||
// TODO what default inmemory size will be enough
|
||||
pipeReaders, pipeWriter, err = sync2.NewTeeInmemory(rs.TotalCount(), memory.MiB.Int64())
|
||||
} else {
|
||||
if tempDir == "" {
|
||||
tempDir = os.TempDir()
|
||||
}
|
||||
pipeReaders, pipeWriter, err = sync2.NewTeeFile(rs.TotalCount(), tempDir)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|