Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
J
jio
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
Tristan Cavelier
jio
Commits
2ee114f1
Commit
2ee114f1
authored
Aug 28, 2013
by
Tristan Cavelier
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
localstorage updated to new jio design
parent
8bf30d16
Changes
1
Show whitespace changes
Inline
Side-by-side
Showing
1 changed file
with
356 additions
and
355 deletions
+356
-355
src/jio.storage/localstorage.js
src/jio.storage/localstorage.js
+356
-355
No files found.
src/jio.storage/localstorage.js
View file @
2ee114f1
...
...
@@ -5,7 +5,8 @@
*/
/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */
/*global jIO, localStorage, setTimeout, complex_queries, define */
/*global jIO, localStorage, setTimeout, complex_queries, window, define,
exports, require */
/**
* JIO Local Storage. Type = 'local'.
...
...
@@ -52,33 +53,17 @@
if
(
typeof
define
===
'
function
'
&&
define
.
amd
)
{
return
define
(
dependencies
,
module
);
}
module
(
jIO
,
complex_queries
);
}([
'
jio
'
,
'
complex_queries
'
],
function
(
jIO
,
complex_queries
)
{
"
use strict
"
;
/**
* Returns 4 hexadecimal random characters.
*
* @return {String} The characters
*/
function
S4
()
{
return
(
'
0000
'
+
Math
.
floor
(
Math
.
random
()
*
0x10000
/* 65536 */
).
toString
(
16
)).
slice
(
-
4
);
}
/**
* An Universal Unique ID generator
*
* @return {String} The new UUID.
*/
function
generateUuid
()
{
return
S4
()
+
S4
()
+
"
-
"
+
S4
()
+
"
-
"
+
S4
()
+
"
-
"
+
S4
()
+
"
-
"
+
S4
()
+
S4
()
+
S4
();
if
(
typeof
exports
===
'
object
'
)
{
return
module
(
exports
,
require
(
'
jio
'
),
require
(
'
complex_queries
'
));
}
window
.
local_storage
=
{};
module
(
window
.
local_storage
,
jIO
,
complex_queries
);
}([
'
exports
'
,
'
jio
'
,
'
complex_queries
'
],
function
(
exports
,
jIO
,
complex_queries
)
{
"
use strict
"
;
/**
* Checks if an object has no enumerable keys
...
...
@@ -132,215 +117,203 @@
}
};
jIO
.
addStorageType
(
'
local
'
,
function
(
spec
,
my
)
{
spec
=
spec
||
{};
var
that
,
priv
;
that
=
my
.
basicStorage
(
spec
,
my
);
priv
=
{};
// attributes
priv
.
username
=
spec
.
username
||
''
;
priv
.
application_name
=
spec
.
application_name
||
'
untitled
'
;
priv
.
localpath
=
'
jio/localstorage/
'
+
priv
.
username
+
'
/
'
+
priv
.
application_name
;
function
LocalStorage
(
spec
)
{
if
(
typeof
spec
.
username
!==
'
string
'
&&
!
spec
.
username
)
{
throw
new
TypeError
(
"
LocalStorage 'username' must be a string
"
+
"
which contains more than one character.
"
);
}
this
.
_localpath
=
'
jio/localstorage/
'
+
spec
.
username
+
'
/
'
+
(
spec
.
application_name
===
null
||
spec
.
application_name
===
undefined
?
'
untitled
'
:
spec
.
application_name
.
toString
()
);
switch
(
spec
.
mode
)
{
case
"
memory
"
:
priv
.
database
=
ram
;
priv
.
storage
=
memorystorage
;
priv
.
mode
=
"
memory
"
;
this
.
_
database
=
ram
;
this
.
_
storage
=
memorystorage
;
this
.
_
mode
=
"
memory
"
;
break
;
default
:
priv
.
database
=
localStorage
;
priv
.
storage
=
localstorage
;
priv
.
mode
=
"
localStorage
"
;
this
.
_
database
=
localStorage
;
this
.
_
storage
=
localstorage
;
this
.
_
mode
=
"
localStorage
"
;
break
;
}
// ===================== overrides ======================
that
.
specToStore
=
function
()
{
return
{
"
application_name
"
:
priv
.
application_name
,
"
username
"
:
priv
.
username
,
"
mode
"
:
priv
.
mode
};
};
that
.
validateState
=
function
()
{
if
(
typeof
priv
.
username
===
"
string
"
&&
priv
.
username
!==
''
)
{
return
''
;
}
return
'
Need at least one parameter: "username".
'
;
};
// ==================== commands ====================
/**
* Create a document in local storage.
*
* @method post
* @param {object} command The JIO command
* @param {Object} command The JIO command
* @param {Object} metadata The metadata to store
* @param {Object} options The command options
*/
that
.
post
=
function
(
command
)
{
setTimeout
(
function
()
{
var
doc
,
doc_id
=
command
.
getDocId
();
LocalStorage
.
prototype
.
post
=
function
(
command
,
metadata
)
{
var
doc
,
doc_id
=
metadata
.
_id
;
if
(
!
doc_id
)
{
doc_id
=
generateUuid
();
doc_id
=
jIO
.
util
.
generateUuid
();
}
doc
=
priv
.
storage
.
getItem
(
priv
.
localpath
+
"
/
"
+
doc_id
);
doc
=
this
.
_storage
.
getItem
(
this
.
_
localpath
+
"
/
"
+
doc_id
);
if
(
doc
===
null
)
{
// the document does not exist
doc
=
command
.
cloneDoc
(
);
doc
=
jIO
.
util
.
deepClone
(
metadata
);
doc
.
_id
=
doc_id
;
delete
doc
.
_attachments
;
priv
.
storage
.
setItem
(
priv
.
localpath
+
"
/
"
+
doc_id
,
doc
);
that
.
success
({
"
ok
"
:
true
,
"
id
"
:
doc_id
});
this
.
_storage
.
setItem
(
this
.
_localpath
+
"
/
"
+
doc_id
,
doc
);
command
.
success
({
"
id
"
:
doc_id
});
}
else
{
// the document already exists
that
.
error
({
"
status
"
:
409
,
"
statusText
"
:
"
Conflicts
"
,
"
error
"
:
"
conflicts
"
,
"
message
"
:
"
Cannot create a new document
"
,
"
reason
"
:
"
Document already exists
"
});
command
.
error
(
"
conflict
"
,
"
document exists
"
,
"
Cannot create a new document
"
);
}
});
};
/**
* Create or update a document in local storage.
*
* @method put
* @param {object} command The JIO command
* @param {Object} command The JIO command
* @param {Object} metadata The metadata to store
* @param {Object} options The command options
*/
that
.
put
=
function
(
command
)
{
setTimeout
(
function
()
{
LocalStorage
.
prototype
.
put
=
function
(
command
,
metadata
)
{
var
doc
,
tmp
;
doc
=
priv
.
storage
.
getItem
(
priv
.
localpath
+
"
/
"
+
command
.
getDocId
()
);
doc
=
this
.
_storage
.
getItem
(
this
.
_localpath
+
"
/
"
+
metadata
.
_id
);
if
(
doc
===
null
)
{
// the document does not exist
doc
=
command
.
cloneDoc
(
);
doc
=
jIO
.
util
.
deepClone
(
metadata
);
delete
doc
.
_attachments
;
}
else
{
// the document already exists
tmp
=
command
.
cloneDoc
(
);
tmp
=
jIO
.
util
.
deepClone
(
metadata
);
tmp
.
_attachments
=
doc
.
_attachments
;
doc
=
tmp
;
}
// write
priv
.
storage
.
setItem
(
priv
.
localpath
+
"
/
"
+
command
.
getDocId
(),
doc
);
that
.
success
({
"
ok
"
:
true
,
"
id
"
:
command
.
getDocId
()
});
});
this
.
_storage
.
setItem
(
this
.
_localpath
+
"
/
"
+
metadata
.
_id
,
doc
);
command
.
success
();
};
/**
* Add an attachment to a document
*
* @method putAttachment
* @param {object} command The JIO command
* @param {Object} command The JIO command
* @param {Object} param The given parameters
* @param {Object} options The command options
*/
that
.
putAttachment
=
function
(
command
)
{
setTimeout
(
function
()
{
var
doc
;
doc
=
priv
.
storage
.
getItem
(
priv
.
localpath
+
"
/
"
+
command
.
getDocId
());
LocalStorage
.
prototype
.
putAttachment
=
function
(
command
,
param
)
{
var
that
=
this
,
doc
;
doc
=
this
.
_storage
.
getItem
(
this
.
_localpath
+
"
/
"
+
param
.
_id
);
if
(
doc
===
null
)
{
// the document does not exist
that
.
error
({
"
status
"
:
404
,
"
statusText
"
:
"
Not Found
"
,
"
error
"
:
"
not_found
"
,
"
message
"
:
"
Impossible to add attachment
"
,
"
reason
"
:
"
Document not found
"
});
return
;
return
command
.
error
(
"
not_found
"
,
"
missing
"
,
"
Impossible to add attachment
"
);
}
// the document already exists
// download data
jIO
.
util
.
blobAsBinaryString
(
param
.
_blob
).
then
(
function
(
data
)
{
doc
.
_attachments
=
doc
.
_attachments
||
{};
doc
.
_attachments
[
command
.
getAttachmentId
()
]
=
{
"
content_type
"
:
command
.
getAttachmentMimeType
()
,
"
digest
"
:
"
md5-
"
+
command
.
md5SumAttachmentData
(
),
"
length
"
:
command
.
getAttachmentLength
()
doc
.
_attachments
[
param
.
_attachment
]
=
{
"
content_type
"
:
param
.
_blob
.
type
,
"
digest
"
:
jIO
.
util
.
makeBinaryStringDigest
(
data
),
"
length
"
:
param
.
_blob
.
size
};
// upload data
priv
.
storage
.
setItem
(
priv
.
localpath
+
"
/
"
+
command
.
getDocId
()
+
"
/
"
+
command
.
getAttachmentId
(),
command
.
getAttachmentData
());
// write document
priv
.
storage
.
setItem
(
priv
.
localpath
+
"
/
"
+
command
.
getDocId
(),
doc
);
that
.
success
({
"
ok
"
:
true
,
"
id
"
:
command
.
getDocId
(),
"
attachment
"
:
command
.
getAttachmentId
()
});
that
.
_storage
.
setItem
(
that
.
_localpath
+
"
/
"
+
param
.
_id
+
"
/
"
+
param
.
_attachment
,
data
);
that
.
_storage
.
setItem
(
that
.
_localpath
+
"
/
"
+
param
.
_id
,
doc
);
command
.
success
({
"
hash
"
:
doc
.
_attachments
[
param
.
_attachment
].
digest
});
},
function
()
{
command
.
error
(
"
request_timeout
"
,
"
blob error
"
,
"
Unable to download blob content
"
);
},
function
()
{
command
.
notify
(
50
);
// XXX get percentage
});
};
/**
* Get a document
*
* @method get
* @param {object} command The JIO command
* @param {Object} command The JIO command
* @param {Object} param The given parameters
* @param {Object} options The command options
*/
that
.
get
=
function
(
command
)
{
setTimeout
(
function
()
{
var
doc
=
priv
.
storage
.
getItem
(
priv
.
localpath
+
"
/
"
+
command
.
getDocId
()
LocalStorage
.
prototype
.
get
=
function
(
command
,
param
)
{
var
doc
=
this
.
_storage
.
getItem
(
this
.
_localpath
+
"
/
"
+
param
.
_id
);
if
(
doc
!==
null
)
{
that
.
success
(
doc
);
command
.
success
({
"
data
"
:
doc
}
);
}
else
{
that
.
error
({
"
status
"
:
404
,
"
statusText
"
:
"
Not Found
"
,
"
error
"
:
"
not_found
"
,
"
message
"
:
"
Cannot find the document
"
,
"
reason
"
:
"
Document does not exist
"
});
command
.
error
(
"
not_found
"
,
"
missing
"
,
"
Cannot find document
"
);
}
});
};
/**
* Get a attachment
* Get an attachment
*
* @method getAttachment
* @param {object} command The JIO command
* @param {Object} command The JIO command
* @param {Object} param The given parameters
* @param {Object} options The command options
*/
that
.
getAttachment
=
function
(
command
)
{
setTimeout
(
function
()
{
var
doc
=
priv
.
storage
.
getItem
(
priv
.
localpath
+
"
/
"
+
command
.
getDocId
()
+
"
/
"
+
command
.
getAttachmentId
()
LocalStorage
.
prototype
.
getAttachment
=
function
(
command
,
param
)
{
var
doc
;
doc
=
this
.
_storage
.
getItem
(
this
.
_localpath
+
"
/
"
+
param
.
_id
);
if
(
doc
===
null
)
{
return
command
.
error
(
"
not_found
"
,
"
missing document
"
,
"
Cannot find document
"
);
}
if
(
typeof
doc
.
_attachments
!==
'
object
'
||
typeof
doc
.
_attachments
[
param
.
_attachment
]
!==
'
object
'
)
{
return
command
.
error
(
"
not_found
"
,
"
missing attachment
"
,
"
Cannot find attachment
"
);
if
(
doc
!==
null
)
{
that
.
success
(
doc
);
}
else
{
that
.
error
({
"
status
"
:
404
,
"
statusText
"
:
"
Not Found
"
,
"
error
"
:
"
not_found
"
,
"
message
"
:
"
Cannot find the attachment
"
,
"
reason
"
:
"
Attachment does not exist
"
});
}
command
.
success
({
"
data
"
:
this
.
_storage
.
getItem
(
this
.
_localpath
+
"
/
"
+
param
.
_id
+
"
/
"
+
param
.
_attachment
)
||
""
,
"
content_type
"
:
doc
.
_attachments
[
param
.
_attachment
].
content_type
||
""
});
};
/**
* Remove a document
*
* @method remove
* @param {object} command The JIO command
* @param {Object} command The JIO command
* @param {Object} param The given parameters
* @param {Object} options The command options
*/
that
.
remove
=
function
(
command
)
{
setTimeout
(
function
()
{
LocalStorage
.
prototype
.
remove
=
function
(
command
,
param
)
{
var
doc
,
i
,
attachment_list
;
doc
=
priv
.
storage
.
getItem
(
priv
.
localpath
+
"
/
"
+
command
.
getDocId
()
);
doc
=
this
.
_storage
.
getItem
(
this
.
_localpath
+
"
/
"
+
param
.
_id
);
attachment_list
=
[];
if
(
doc
!==
null
&&
typeof
doc
===
"
object
"
)
{
if
(
typeof
doc
.
_attachments
===
"
object
"
)
{
...
...
@@ -352,131 +325,117 @@
}
}
}
else
{
return
that
.
error
({
"
status
"
:
404
,
"
statusText
"
:
"
Not Found
"
,
"
error
"
:
"
not_found
"
,
"
message
"
:
"
Document not found
"
,
"
reason
"
:
"
missing
"
});
return
command
.
error
(
"
not_found
"
,
"
missing
"
,
"
Document not found
"
);
}
priv
.
storage
.
removeItem
(
priv
.
localpath
+
"
/
"
+
command
.
getDocId
()
);
this
.
_storage
.
removeItem
(
this
.
_localpath
+
"
/
"
+
param
.
_id
);
// delete all attachments
for
(
i
=
0
;
i
<
attachment_list
.
length
;
i
+=
1
)
{
priv
.
storage
.
removeItem
(
priv
.
localpath
+
"
/
"
+
command
.
getDocId
()
+
this
.
_storage
.
removeItem
(
this
.
_localpath
+
"
/
"
+
param
.
_id
+
"
/
"
+
attachment_list
[
i
]);
}
that
.
success
({
"
ok
"
:
true
,
"
id
"
:
command
.
getDocId
()
});
});
command
.
success
();
};
/**
* Remove an attachment
*
* @method removeAttachment
* @param {object} command The JIO command
* @param {Object} command The JIO command
* @param {Object} param The given parameters
* @param {Object} options The command options
*/
that
.
removeAttachment
=
function
(
command
)
{
setTimeout
(
function
()
{
var
doc
,
error
;
error
=
function
(
word
)
{
that
.
error
({
"
status
"
:
404
,
"
statusText
"
:
"
Not Found
"
,
"
error
"
:
"
not_found
"
,
"
message
"
:
word
+
"
not found
"
,
"
reason
"
:
"
missing
"
});
};
doc
=
priv
.
storage
.
getItem
(
priv
.
localpath
+
"
/
"
+
command
.
getDocId
());
// remove attachment from document
if
(
doc
!==
null
&&
typeof
doc
===
"
object
"
&&
typeof
doc
.
_attachments
===
"
object
"
)
{
if
(
typeof
doc
.
_attachments
[
command
.
getAttachmentId
()]
===
"
object
"
)
{
delete
doc
.
_attachments
[
command
.
getAttachmentId
()];
if
(
objectIsEmpty
(
doc
.
_attachments
))
{
delete
doc
.
_attachments
;
LocalStorage
.
prototype
.
removeAttachment
=
function
(
command
,
param
)
{
var
doc
=
this
.
_storage
.
getItem
(
this
.
_localpath
+
"
/
"
+
param
.
_id
);
if
(
typeof
doc
!==
'
object
'
)
{
return
command
.
error
(
"
not_found
"
,
"
missing document
"
,
"
Document not found
"
);
}
priv
.
storage
.
setItem
(
priv
.
localpath
+
"
/
"
+
command
.
getDocId
(),
doc
);
priv
.
storage
.
removeItem
(
priv
.
localpath
+
"
/
"
+
command
.
getDocId
()
+
"
/
"
+
command
.
getAttachmentId
());
that
.
success
({
"
ok
"
:
true
,
"
id
"
:
command
.
getDocId
(),
"
attachment
"
:
command
.
getAttachmentId
()
});
}
else
{
error
(
"
Attachment
"
);
if
(
typeof
doc
.
_attachments
!==
"
object
"
||
typeof
doc
.
_attachments
[
param
.
_attachment
]
!==
"
object
"
)
{
return
command
.
error
(
"
not_found
"
,
"
missing attachment
"
,
"
Attachment not found
"
);
}
}
else
{
error
(
"
Document
"
);
delete
doc
.
_attachments
[
param
.
_attachment
];
if
(
objectIsEmpty
(
doc
.
_attachments
))
{
delete
doc
.
_attachments
;
}
});
this
.
_storage
.
setItem
(
this
.
_localpath
+
"
/
"
+
param
.
_id
,
doc
);
this
.
_storage
.
removeItem
(
this
.
_localpath
+
"
/
"
+
param
.
_id
+
"
/
"
+
param
.
_attachment
);
command
.
success
();
};
/**
* Get all filenames belonging to a user from the document index
*
* @method allDocs
* @param {object} command The JIO command
* @param {Object} command The JIO command
* @param {Object} param The given parameters
* @param {Object} options The command options
*/
that
.
allDocs
=
function
(
command
)
{
var
i
,
row
,
path_re
,
rows
,
document_list
,
option
,
document_object
;
LocalStorage
.
prototype
.
allDocs
=
function
(
command
,
param
,
options
)
{
var
i
,
row
,
path_re
,
rows
,
document_list
,
document_object
;
rows
=
[];
document_list
=
[];
path_re
=
new
RegExp
(
"
^
"
+
complex_queries
.
stringEscapeRegexpCharacters
(
priv
.
localpath
)
+
"
^
"
+
complex_queries
.
stringEscapeRegexpCharacters
(
this
.
_
localpath
)
+
"
/[^/]+$
"
);
option
=
command
.
cloneOption
();
if
(
typeof
complex_queries
!==
"
object
"
||
(
option
.
query
===
undefined
&&
option
.
sort_on
===
undefined
&&
option
.
select_list
===
undefined
&&
option
.
include_docs
===
undefined
))
{
if
(
options
.
query
===
undefined
&&
options
.
sort_on
===
undefined
&&
options
.
select_list
===
undefined
&&
options
.
include_docs
===
undefined
)
{
rows
=
[];
for
(
i
in
priv
.
database
)
{
if
(
priv
.
database
.
hasOwnProperty
(
i
))
{
for
(
i
in
this
.
_
database
)
{
if
(
this
.
_
database
.
hasOwnProperty
(
i
))
{
// filter non-documents
if
(
path_re
.
test
(
i
))
{
row
=
{
value
:
{}
};
row
.
id
=
i
.
split
(
'
/
'
).
slice
(
-
1
)[
0
];
row
.
key
=
row
.
id
;
if
(
command
.
getOption
(
'
include_docs
'
)
)
{
row
.
doc
=
JSON
.
parse
(
priv
.
storage
.
getItem
(
i
));
if
(
options
.
include_docs
)
{
row
.
doc
=
JSON
.
parse
(
this
.
_
storage
.
getItem
(
i
));
}
rows
.
push
(
row
);
}
}
}
that
.
success
({
"
rows
"
:
rows
,
"
total_rows
"
:
rows
.
length
});
command
.
success
({
"
data
"
:
{
"
rows
"
:
rows
,
"
total_rows
"
:
rows
.
length
}
});
}
else
{
// create complex query object from returned results
for
(
i
in
priv
.
database
)
{
if
(
priv
.
database
.
hasOwnProperty
(
i
))
{
for
(
i
in
this
.
_
database
)
{
if
(
this
.
_
database
.
hasOwnProperty
(
i
))
{
if
(
path_re
.
test
(
i
))
{
document_list
.
push
(
priv
.
storage
.
getItem
(
i
));
document_list
.
push
(
this
.
_
storage
.
getItem
(
i
));
}
}
}
option
.
select_list
=
option
.
select_list
||
[];
option
.
select_list
.
push
(
"
_id
"
);
if
(
option
.
include_docs
===
true
)
{
options
.
select_list
=
options
.
select_list
||
[];
options
.
select_list
.
push
(
"
_id
"
);
if
(
options
.
include_docs
===
true
)
{
document_object
=
{};
document_list
.
forEach
(
function
(
meta
)
{
document_object
[
meta
.
_id
]
=
meta
;
});
}
complex_queries
.
QueryFactory
.
create
(
option
.
query
||
""
).
exec
(
document_list
,
option
);
complex_queries
.
QueryFactory
.
create
(
options
.
query
||
""
).
exec
(
document_list
,
options
);
document_list
=
document_list
.
map
(
function
(
value
)
{
var
o
=
{
"
id
"
:
value
.
_id
,
"
key
"
:
value
.
_id
};
if
(
option
.
include_docs
===
true
)
{
if
(
options
.
include_docs
===
true
)
{
o
.
doc
=
document_object
[
value
.
_id
];
delete
document_object
[
value
.
_id
];
}
...
...
@@ -484,11 +443,53 @@
o
.
value
=
value
;
return
o
;
});
that
.
success
({
"
total_rows
"
:
document_list
.
length
,
"
rows
"
:
document_list
});
command
.
success
({
"
data
"
:
{
"
total_rows
"
:
document_list
.
length
,
"
rows
"
:
document_list
}});
}
};
return
that
;
});
jIO
.
addStorage
(
'
local
'
,
LocalStorage
);
//////////////////////////////////////////////////////////////////////
// Tools
/**
* Tool to help users to create local storage description for JIO
*
* @param {String} username The username
* @param {String} [application_name] The application_name
* @return {Object} The storage description
*/
function
createDescription
(
username
,
application_name
)
{
var
description
=
{
"
type
"
:
"
local
"
,
"
username
"
:
username
.
toString
()
};
if
(
application_name
!==
undefined
)
{
description
.
application_name
=
application_name
.
toString
();
}
return
description
;
}
exports
.
createDescription
=
createDescription
;
function
clear
()
{
var
k
;
for
(
k
in
localStorage
)
{
if
(
localStorage
.
hasOwnProperty
(
k
))
{
if
(
/^jio
\/
localstorage
\/
/
.
test
(
k
))
{
localStorage
.
removeItem
(
k
);
}
}
}
}
exports
.
clear
=
clear
;
exports
.
clearLocalStorage
=
clear
;
function
clearMemoryStorage
()
{
jIO
.
util
.
dictClear
(
ram
);
}
exports
.
clearMemoryStorage
=
clearMemoryStorage
;
}));
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment