Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
W
wiki-js
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
1
Issues
1
List
Board
Labels
Milestones
Merge Requests
1
Merge Requests
1
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Jacklull
wiki-js
Commits
d9870583
Commit
d9870583
authored
Oct 19, 2019
by
NGPixel
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: asset add/rename/remove + dump action for git and disk modules
parent
73aa870a
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
128 additions
and
30 deletions
+128
-30
nav-header.vue
client/components/common/nav-header.vue
+4
-4
upload.js
server/controllers/upload.js
+1
-1
asset.js
server/graph/resolvers/asset.js
+13
-4
assetFolders.js
server/models/assetFolders.js
+19
-1
assets.js
server/models/assets.js
+19
-12
storage.js
server/modules/storage/disk/storage.js
+47
-1
storage.js
server/modules/storage/git/storage.js
+25
-7
No files found.
client/components/common/nav-header.vue
View file @
d9870583
...
@@ -91,7 +91,7 @@
...
@@ -91,7 +91,7 @@
//- LANGUAGES
//- LANGUAGES
template(v-if='mode === `view` && locales.length > 0')
template(v-if='mode === `view` && locales.length > 0')
v-menu(offset-y, bottom,
nudge-bottom='30',
transition='slide-y-transition')
v-menu(offset-y, bottom, transition='slide-y-transition')
template(v-slot:activator='{ on: menu }')
template(v-slot:activator='{ on: menu }')
v-tooltip(bottom)
v-tooltip(bottom)
template(v-slot:activator='{ on: tooltip }')
template(v-slot:activator='{ on: tooltip }')
...
@@ -101,14 +101,14 @@
...
@@ -101,14 +101,14 @@
v-list(nav)
v-list(nav)
template(v-for='(lc, idx) of locales')
template(v-for='(lc, idx) of locales')
v-list-item(@click='changeLocale(lc)')
v-list-item(@click='changeLocale(lc)')
v-list-item-action: v-chip(:color='lc.code === locale ? `blue` : `grey`', small, label, dark)
{{
lc
.
code
.
toUpperCase
()
}}
v-list-item-action
(style='min-width:auto;')
: v-chip(:color='lc.code === locale ? `blue` : `grey`', small, label, dark)
{{
lc
.
code
.
toUpperCase
()
}}
v-list-item-title
{{
lc
.
name
}}
v-list-item-title
{{
lc
.
name
}}
v-divider(vertical)
v-divider(vertical)
//- PAGE ACTIONS
//- PAGE ACTIONS
template(v-if='isAuthenticated && path && mode !== `edit`')
template(v-if='isAuthenticated && path && mode !== `edit`')
v-menu(offset-y, bottom,
nudge-bottom='30',
transition='slide-y-transition')
v-menu(offset-y, bottom, transition='slide-y-transition')
template(v-slot:activator='{ on: menu }')
template(v-slot:activator='{ on: menu }')
v-tooltip(bottom)
v-tooltip(bottom)
template(v-slot:activator='{ on: tooltip }')
template(v-slot:activator='{ on: tooltip }')
...
@@ -152,7 +152,7 @@
...
@@ -152,7 +152,7 @@
//- ACCOUNT
//- ACCOUNT
v-menu(v-if='isAuthenticated', offset-y, bottom,
nudge-bottom='30',
min-width='300', transition='slide-y-transition')
v-menu(v-if='isAuthenticated', offset-y, bottom, min-width='300', transition='slide-y-transition')
template(v-slot:activator='{ on: menu }')
template(v-slot:activator='{ on: menu }')
v-tooltip(bottom)
v-tooltip(bottom)
template(v-slot:activator='{ on: tooltip }')
template(v-slot:activator='{ on: tooltip }')
...
...
server/controllers/upload.js
View file @
d9870583
...
@@ -90,7 +90,7 @@ router.post('/u', multer({
...
@@ -90,7 +90,7 @@ router.post('/u', multer({
...
fileMeta
,
...
fileMeta
,
folderId
:
folderId
,
folderId
:
folderId
,
assetPath
,
assetPath
,
user
Id
:
req
.
user
.
id
user
:
req
.
user
})
})
res
.
send
(
'ok'
)
res
.
send
(
'ok'
)
})
})
...
...
server/graph/resolvers/asset.js
View file @
d9870583
...
@@ -123,8 +123,11 @@ module.exports = {
...
@@ -123,8 +123,11 @@ module.exports = {
event
:
'renamed'
,
event
:
'renamed'
,
asset
:
{
asset
:
{
...
asset
,
...
asset
,
sourcePath
:
assetSourcePath
,
path
:
assetSourcePath
,
destinationPath
:
assetTargetPath
destinationPath
:
assetTargetPath
,
moveAuthorId
:
context
.
req
.
user
.
id
,
moveAuthorName
:
context
.
req
.
user
.
name
,
moveAuthorEmail
:
context
.
req
.
user
.
email
}
}
})
})
...
@@ -146,7 +149,7 @@ module.exports = {
...
@@ -146,7 +149,7 @@ module.exports = {
const
asset
=
await
WIKI
.
models
.
assets
.
query
().
findById
(
args
.
id
)
const
asset
=
await
WIKI
.
models
.
assets
.
query
().
findById
(
args
.
id
)
if
(
asset
)
{
if
(
asset
)
{
// Check permissions
// Check permissions
const
assetPath
=
asset
.
getAssetPath
()
const
assetPath
=
a
wait
a
sset
.
getAssetPath
()
if
(
!
WIKI
.
auth
.
checkAccess
(
context
.
req
.
user
,
[
'manage:assets'
],
{
path
:
assetPath
}))
{
if
(
!
WIKI
.
auth
.
checkAccess
(
context
.
req
.
user
,
[
'manage:assets'
],
{
path
:
assetPath
}))
{
throw
new
WIKI
.
Error
.
AssetDeleteForbidden
()
throw
new
WIKI
.
Error
.
AssetDeleteForbidden
()
}
}
...
@@ -158,7 +161,13 @@ module.exports = {
...
@@ -158,7 +161,13 @@ module.exports = {
// Delete from Storage
// Delete from Storage
await
WIKI
.
models
.
storage
.
assetEvent
({
await
WIKI
.
models
.
storage
.
assetEvent
({
event
:
'deleted'
,
event
:
'deleted'
,
asset
asset
:
{
...
asset
,
path
:
assetPath
,
authorId
:
context
.
req
.
user
.
id
,
authorName
:
context
.
req
.
user
.
name
,
authorEmail
:
context
.
req
.
user
.
email
}
})
})
return
{
return
{
...
...
server/models/assetFolders.js
View file @
d9870583
...
@@ -39,7 +39,7 @@ module.exports = class AssetFolder extends Model {
...
@@ -39,7 +39,7 @@ module.exports = class AssetFolder extends Model {
*
*
* @param {Number} folderId Id of the folder
* @param {Number} folderId Id of the folder
*/
*/
static
async
getHierarchy
(
folderId
)
{
static
async
getHierarchy
(
folderId
)
{
const
hier
=
await
WIKI
.
models
.
knex
.
withRecursive
(
'ancestors'
,
qb
=>
{
const
hier
=
await
WIKI
.
models
.
knex
.
withRecursive
(
'ancestors'
,
qb
=>
{
qb
.
select
(
'id'
,
'name'
,
'slug'
,
'parentId'
).
from
(
'assetFolders'
).
where
(
'id'
,
folderId
).
union
(
sqb
=>
{
qb
.
select
(
'id'
,
'name'
,
'slug'
,
'parentId'
).
from
(
'assetFolders'
).
where
(
'id'
,
folderId
).
union
(
sqb
=>
{
sqb
.
select
(
'a.id'
,
'a.name'
,
'a.slug'
,
'a.parentId'
).
from
(
'assetFolders AS a'
).
join
(
'ancestors'
,
'ancestors.parentId'
,
'a.id'
)
sqb
.
select
(
'a.id'
,
'a.name'
,
'a.slug'
,
'a.parentId'
).
from
(
'assetFolders AS a'
).
join
(
'ancestors'
,
'ancestors.parentId'
,
'a.id'
)
...
@@ -48,4 +48,22 @@ module.exports = class AssetFolder extends Model {
...
@@ -48,4 +48,22 @@ module.exports = class AssetFolder extends Model {
// The ancestors are from children to grandparents, must reverse for correct path order.
// The ancestors are from children to grandparents, must reverse for correct path order.
return
_
.
reverse
(
hier
)
return
_
.
reverse
(
hier
)
}
}
/**
* Get full folder paths
*/
static
async
getAllPaths
()
{
const
all
=
await
WIKI
.
models
.
assetFolders
.
query
()
let
folders
=
{}
all
.
forEach
(
fld
=>
{
_
.
set
(
folders
,
fld
.
id
,
fld
.
slug
)
let
parentId
=
fld
.
parentId
while
(
parentId
!==
null
||
parentId
>
0
)
{
const
parent
=
_
.
find
(
all
,
[
'id'
,
parentId
])
_
.
set
(
folders
,
fld
.
id
,
`
${
parent
.
slug
}
/
${
_
.
get
(
folders
,
fld
.
id
)}
`
)
parentId
=
parent
.
parentId
}
})
return
folders
}
}
}
server/models/assets.js
View file @
d9870583
...
@@ -96,7 +96,7 @@ module.exports = class Asset extends Model {
...
@@ -96,7 +96,7 @@ module.exports = class Asset extends Model {
mime
:
opts
.
mimetype
,
mime
:
opts
.
mimetype
,
fileSize
:
opts
.
size
,
fileSize
:
opts
.
size
,
folderId
:
opts
.
folderId
,
folderId
:
opts
.
folderId
,
authorId
:
opts
.
user
I
d
authorId
:
opts
.
user
.
i
d
}
}
// Save asset data
// Save asset data
...
@@ -119,20 +119,27 @@ module.exports = class Asset extends Model {
...
@@ -119,20 +119,27 @@ module.exports = class Asset extends Model {
data
:
fileBuffer
data
:
fileBuffer
})
})
}
}
// Move temp upload to cache
await
fs
.
move
(
opts
.
path
,
path
.
join
(
process
.
cwd
(),
`data/cache/
${
fileHash
}
.dat`
),
{
overwrite
:
true
})
// Add to Storage
if
(
!
opts
.
skipStorage
)
{
await
WIKI
.
models
.
storage
.
assetEvent
({
event
:
'uploaded'
,
asset
:
{
...
asset
,
path
:
await
asset
.
getAssetPath
(),
data
:
fileBuffer
,
authorId
:
opts
.
user
.
id
,
authorName
:
opts
.
user
.
name
,
authorEmail
:
opts
.
user
.
email
}
})
}
}
catch
(
err
)
{
}
catch
(
err
)
{
WIKI
.
logger
.
warn
(
err
)
WIKI
.
logger
.
warn
(
err
)
}
}
// Move temp upload to cache
await
fs
.
move
(
opts
.
path
,
path
.
join
(
process
.
cwd
(),
`data/cache/
${
fileHash
}
.dat`
),
{
overwrite
:
true
})
// Add to Storage
if
(
!
opts
.
skipStorage
)
{
await
WIKI
.
models
.
storage
.
assetEvent
({
event
:
'uploaded'
,
asset
})
}
}
}
static
async
getAsset
(
assetPath
,
res
)
{
static
async
getAsset
(
assetPath
,
res
)
{
...
...
server/modules/storage/disk/storage.js
View file @
d9870583
...
@@ -88,12 +88,41 @@ module.exports = {
...
@@ -88,12 +88,41 @@ module.exports = {
await
fs
.
move
(
path
.
join
(
this
.
config
.
path
,
sourceFilePath
),
path
.
join
(
this
.
config
.
path
,
destinationFilePath
),
{
overwrite
:
true
})
await
fs
.
move
(
path
.
join
(
this
.
config
.
path
,
sourceFilePath
),
path
.
join
(
this
.
config
.
path
,
destinationFilePath
),
{
overwrite
:
true
})
},
},
/**
* ASSET UPLOAD
*
* @param {Object} asset Asset to upload
*/
async
assetUploaded
(
asset
)
{
WIKI
.
logger
.
info
(
`(STORAGE/DISK) Creating new file
${
asset
.
path
}
...`
)
await
fs
.
outputFile
(
path
.
join
(
this
.
config
.
path
,
asset
.
path
),
asset
.
data
)
},
/**
* ASSET DELETE
*
* @param {Object} asset Asset to delete
*/
async
assetDeleted
(
asset
)
{
WIKI
.
logger
.
info
(
`(STORAGE/DISK) Deleting file
${
asset
.
path
}
...`
)
await
fs
.
remove
(
path
.
join
(
this
.
config
.
path
,
asset
.
path
))
},
/**
* ASSET RENAME
*
* @param {Object} asset Asset to rename
*/
async
assetRenamed
(
asset
)
{
WIKI
.
logger
.
info
(
`(STORAGE/DISK) Renaming file from
${
asset
.
path
}
to
${
asset
.
destinationPath
}
...`
)
await
fs
.
move
(
path
.
join
(
this
.
config
.
path
,
asset
.
path
),
path
.
join
(
this
.
config
.
path
,
asset
.
destinationPath
),
{
overwrite
:
true
})
},
/**
/**
* HANDLERS
* HANDLERS
*/
*/
async
dump
()
{
async
dump
()
{
WIKI
.
logger
.
info
(
`(STORAGE/DISK) Dumping all content to disk...`
)
WIKI
.
logger
.
info
(
`(STORAGE/DISK) Dumping all content to disk...`
)
// -> Pages
await
pipeline
(
await
pipeline
(
WIKI
.
models
.
knex
.
column
(
'path'
,
'localeCode'
,
'title'
,
'description'
,
'contentType'
,
'content'
,
'isPublished'
,
'updatedAt'
).
select
().
from
(
'pages'
).
where
({
WIKI
.
models
.
knex
.
column
(
'path'
,
'localeCode'
,
'title'
,
'description'
,
'contentType'
,
'content'
,
'isPublished'
,
'updatedAt'
).
select
().
from
(
'pages'
).
where
({
isPrivate
:
false
isPrivate
:
false
...
@@ -105,13 +134,30 @@ module.exports = {
...
@@ -105,13 +134,30 @@ module.exports = {
if
(
WIKI
.
config
.
lang
.
code
!==
page
.
localeCode
)
{
if
(
WIKI
.
config
.
lang
.
code
!==
page
.
localeCode
)
{
fileName
=
`
${
page
.
localeCode
}
/
${
fileName
}
`
fileName
=
`
${
page
.
localeCode
}
/
${
fileName
}
`
}
}
WIKI
.
logger
.
info
(
`(STORAGE/DISK) Dumping
${
fileName
}
...`
)
WIKI
.
logger
.
info
(
`(STORAGE/DISK) Dumping
page
${
fileName
}
...`
)
const
filePath
=
path
.
join
(
this
.
config
.
path
,
fileName
)
const
filePath
=
path
.
join
(
this
.
config
.
path
,
fileName
)
await
fs
.
outputFile
(
filePath
,
pageHelper
.
injectPageMetadata
(
page
),
'utf8'
)
await
fs
.
outputFile
(
filePath
,
pageHelper
.
injectPageMetadata
(
page
),
'utf8'
)
cb
()
cb
()
}
}
})
})
)
)
// -> Assets
const
assetFolders
=
await
WIKI
.
models
.
assetFolders
.
getAllPaths
()
await
pipeline
(
WIKI
.
models
.
knex
.
column
(
'filename'
,
'folderId'
,
'data'
).
select
().
from
(
'assets'
).
join
(
'assetData'
,
'assets.id'
,
'='
,
'assetData.id'
).
stream
(),
new
stream
.
Transform
({
objectMode
:
true
,
transform
:
async
(
asset
,
enc
,
cb
)
=>
{
const
filename
=
(
asset
.
folderId
&&
asset
.
folderId
>
0
)
?
`
${
_
.
get
(
assetFolders
,
asset
.
folderId
)}
/
${
asset
.
filename
}
`
:
asset
.
filename
WIKI
.
logger
.
info
(
`(STORAGE/DISK) Dumping asset
${
filename
}
...`
)
await
fs
.
outputFile
(
path
.
join
(
this
.
config
.
path
,
filename
),
asset
.
data
)
cb
()
}
})
)
WIKI
.
logger
.
info
(
'(STORAGE/DISK) All content was dumped to disk successfully.'
)
WIKI
.
logger
.
info
(
'(STORAGE/DISK) All content was dumped to disk successfully.'
)
},
},
async
backup
()
{
async
backup
()
{
...
...
server/modules/storage/git/storage.js
View file @
d9870583
...
@@ -283,7 +283,7 @@ module.exports = {
...
@@ -283,7 +283,7 @@ module.exports = {
}
}
await
this
.
git
.
mv
(
`./
${
sourceFilePath
}
`
,
`./
${
destinationFilePath
}
`
)
await
this
.
git
.
mv
(
`./
${
sourceFilePath
}
`
,
`./
${
destinationFilePath
}
`
)
await
this
.
git
.
commit
(
`docs: rename
${
page
.
path
}
to
${
page
.
destinationPath
}
`
,
destinationFilePath
,
{
await
this
.
git
.
commit
(
`docs: rename
${
page
.
path
}
to
${
page
.
destinationPath
}
`
,
[
sourceFilePath
,
destinationFilePath
]
,
{
'--author'
:
`"
${
page
.
moveAuthorName
}
<
${
page
.
moveAuthorEmail
}
>"`
'--author'
:
`"
${
page
.
moveAuthorName
}
<
${
page
.
moveAuthorEmail
}
>"`
})
})
},
},
...
@@ -295,7 +295,7 @@ module.exports = {
...
@@ -295,7 +295,7 @@ module.exports = {
async
assetUploaded
(
asset
)
{
async
assetUploaded
(
asset
)
{
WIKI
.
logger
.
info
(
`(STORAGE/GIT) Committing new file
${
asset
.
path
}
...`
)
WIKI
.
logger
.
info
(
`(STORAGE/GIT) Committing new file
${
asset
.
path
}
...`
)
const
filePath
=
path
.
join
(
this
.
repoPath
,
asset
.
path
)
const
filePath
=
path
.
join
(
this
.
repoPath
,
asset
.
path
)
await
fs
.
outputFile
(
filePath
,
asset
,
'utf8'
)
await
fs
.
outputFile
(
filePath
,
asset
.
data
,
'utf8'
)
await
this
.
git
.
add
(
`./
${
asset
.
path
}
`
)
await
this
.
git
.
add
(
`./
${
asset
.
path
}
`
)
await
this
.
git
.
commit
(
`docs: upload
${
asset
.
path
}
`
,
asset
.
path
,
{
await
this
.
git
.
commit
(
`docs: upload
${
asset
.
path
}
`
,
asset
.
path
,
{
...
@@ -321,11 +321,11 @@ module.exports = {
...
@@ -321,11 +321,11 @@ module.exports = {
* @param {Object} asset Asset to upload
* @param {Object} asset Asset to upload
*/
*/
async
assetRenamed
(
asset
)
{
async
assetRenamed
(
asset
)
{
WIKI
.
logger
.
info
(
`(STORAGE/GIT) Committing file move from
${
asset
.
sourceP
ath
}
to
${
asset
.
destinationPath
}
...`
)
WIKI
.
logger
.
info
(
`(STORAGE/GIT) Committing file move from
${
asset
.
p
ath
}
to
${
asset
.
destinationPath
}
...`
)
await
this
.
git
.
mv
(
`./
${
asset
.
sourceP
ath
}
`
,
`./
${
asset
.
destinationPath
}
`
)
await
this
.
git
.
mv
(
`./
${
asset
.
p
ath
}
`
,
`./
${
asset
.
destinationPath
}
`
)
await
this
.
git
.
commit
(
`docs: rename
${
asset
.
sourcePath
}
to
${
asset
.
destinationPath
}
`
,
asset
.
destinationPath
,
{
await
this
.
git
.
commit
(
`docs: rename
${
asset
.
path
}
to
${
asset
.
destinationPath
}
`
,
[
asset
.
path
,
asset
.
destinationPath
]
,
{
'--author'
:
`"
${
asset
.
authorName
}
<
${
asset
.
a
uthorEmail
}
>"`
'--author'
:
`"
${
asset
.
moveAuthorName
}
<
${
asset
.
moveA
uthorEmail
}
>"`
})
})
},
},
/**
/**
...
@@ -364,6 +364,7 @@ module.exports = {
...
@@ -364,6 +364,7 @@ module.exports = {
async
syncUntracked
()
{
async
syncUntracked
()
{
WIKI
.
logger
.
info
(
`(STORAGE/GIT) Adding all untracked content...`
)
WIKI
.
logger
.
info
(
`(STORAGE/GIT) Adding all untracked content...`
)
// -> Pages
await
pipeline
(
await
pipeline
(
WIKI
.
models
.
knex
.
column
(
'path'
,
'localeCode'
,
'title'
,
'description'
,
'contentType'
,
'content'
,
'isPublished'
,
'updatedAt'
).
select
().
from
(
'pages'
).
where
({
WIKI
.
models
.
knex
.
column
(
'path'
,
'localeCode'
,
'title'
,
'description'
,
'contentType'
,
'content'
,
'isPublished'
,
'updatedAt'
).
select
().
from
(
'pages'
).
where
({
isPrivate
:
false
isPrivate
:
false
...
@@ -375,7 +376,7 @@ module.exports = {
...
@@ -375,7 +376,7 @@ module.exports = {
if
(
WIKI
.
config
.
lang
.
namespacing
&&
WIKI
.
config
.
lang
.
code
!==
page
.
localeCode
)
{
if
(
WIKI
.
config
.
lang
.
namespacing
&&
WIKI
.
config
.
lang
.
code
!==
page
.
localeCode
)
{
fileName
=
`
${
page
.
localeCode
}
/
${
fileName
}
`
fileName
=
`
${
page
.
localeCode
}
/
${
fileName
}
`
}
}
WIKI
.
logger
.
info
(
`(STORAGE/GIT) Adding
${
fileName
}
...`
)
WIKI
.
logger
.
info
(
`(STORAGE/GIT) Adding
page
${
fileName
}
...`
)
const
filePath
=
path
.
join
(
this
.
repoPath
,
fileName
)
const
filePath
=
path
.
join
(
this
.
repoPath
,
fileName
)
await
fs
.
outputFile
(
filePath
,
pageHelper
.
injectPageMetadata
(
page
),
'utf8'
)
await
fs
.
outputFile
(
filePath
,
pageHelper
.
injectPageMetadata
(
page
),
'utf8'
)
await
this
.
git
.
add
(
`./
${
fileName
}
`
)
await
this
.
git
.
add
(
`./
${
fileName
}
`
)
...
@@ -383,6 +384,23 @@ module.exports = {
...
@@ -383,6 +384,23 @@ module.exports = {
}
}
})
})
)
)
// -> Assets
const
assetFolders
=
await
WIKI
.
models
.
assetFolders
.
getAllPaths
()
await
pipeline
(
WIKI
.
models
.
knex
.
column
(
'filename'
,
'folderId'
,
'data'
).
select
().
from
(
'assets'
).
join
(
'assetData'
,
'assets.id'
,
'='
,
'assetData.id'
).
stream
(),
new
stream
.
Transform
({
objectMode
:
true
,
transform
:
async
(
asset
,
enc
,
cb
)
=>
{
const
filename
=
(
asset
.
folderId
&&
asset
.
folderId
>
0
)
?
`
${
_
.
get
(
assetFolders
,
asset
.
folderId
)}
/
${
asset
.
filename
}
`
:
asset
.
filename
WIKI
.
logger
.
info
(
`(STORAGE/GIT) Adding asset
${
filename
}
...`
)
await
fs
.
outputFile
(
path
.
join
(
this
.
repoPath
,
filename
),
asset
.
data
)
await
this
.
git
.
add
(
`./
${
filename
}
`
)
cb
()
}
})
)
await
this
.
git
.
commit
(
`docs: add all untracked content`
)
await
this
.
git
.
commit
(
`docs: add all untracked content`
)
WIKI
.
logger
.
info
(
'(STORAGE/GIT) All content is now tracked.'
)
WIKI
.
logger
.
info
(
'(STORAGE/GIT) All content is now tracked.'
)
}
}
...
...
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