Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
S
slm-fileview
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
梁杰芳
slm-fileview
Commits
bcdb5ce0
Commit
bcdb5ce0
authored
Apr 18, 2021
by
zhangxiaoxiao9527
Committed by
kl
Apr 19, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
集成视频格式转换功能1.0(基于javacv)
parent
a3485dd9
Show whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
332 additions
and
2 deletions
+332
-2
pom.xml
server/pom.xml
+58
-0
application.properties
server/src/main/config/application.properties
+5
-0
ConfigConstants.java
server/src/main/java/cn/keking/config/ConfigConstants.java
+29
-0
FileType.java
server/src/main/java/cn/keking/model/FileType.java
+4
-0
FileHandlerService.java
...r/src/main/java/cn/keking/service/FileHandlerService.java
+23
-0
CacheService.java
...r/src/main/java/cn/keking/service/cache/CacheService.java
+6
-0
CacheServiceJDKImpl.java
...ava/cn/keking/service/cache/impl/CacheServiceJDKImpl.java
+25
-0
CacheServiceRedisImpl.java
...a/cn/keking/service/cache/impl/CacheServiceRedisImpl.java
+22
-0
CacheServiceRocksDBImpl.java
...cn/keking/service/cache/impl/CacheServiceRocksDBImpl.java
+39
-0
MediaFilePreviewImpl.java
...ain/java/cn/keking/service/impl/MediaFilePreviewImpl.java
+121
-2
No files found.
server/pom.xml
View file @
bcdb5ce0
...
...
@@ -192,6 +192,64 @@
<artifactId>
galimatias
</artifactId>
<version>
0.2.1
</version>
</dependency>
<!-- 以下是bytedeco 基于opencv ffmpeg封装的javacv,用于视频处理 -->
<dependency>
<groupId>
org.bytedeco
</groupId>
<artifactId>
javacv
</artifactId>
<version>
1.5.2
</version>
</dependency>
<dependency>
<groupId>
org.bytedeco
</groupId>
<artifactId>
javacpp
</artifactId>
<version>
1.5.2
</version>
</dependency>
<!-- 此版本中主要兼容linux和windows系统,如需兼容其他系统平台,请引入对应依赖即可 -->
<dependency>
<groupId>
org.bytedeco
</groupId>
<artifactId>
opencv
</artifactId>
<version>
4.1.2-1.5.2
</version>
<classifier>
linux-x86_64
</classifier>
</dependency>
<dependency>
<groupId>
org.bytedeco
</groupId>
<artifactId>
opencv
</artifactId>
<version>
4.1.2-1.5.2
</version>
<classifier>
windows-x86_64
</classifier>
</dependency>
<dependency>
<groupId>
org.bytedeco
</groupId>
<artifactId>
openblas
</artifactId>
<version>
0.3.6-1.5.1
</version>
<classifier>
linux-x86_64
</classifier>
</dependency>
<dependency>
<groupId>
org.bytedeco
</groupId>
<artifactId>
openblas
</artifactId>
<version>
0.3.6-1.5.1
</version>
<classifier>
windows-x86_64
</classifier>
</dependency>
<dependency>
<groupId>
org.bytedeco
</groupId>
<artifactId>
ffmpeg
</artifactId>
<version>
4.2.1-1.5.2
</version>
<classifier>
linux-x86_64
</classifier>
</dependency>
<dependency>
<groupId>
org.bytedeco
</groupId>
<artifactId>
ffmpeg
</artifactId>
<version>
4.2.1-1.5.2
</version>
<classifier>
windows-x86_64
</classifier>
</dependency>
</dependencies>
<build>
...
...
server/src/main/config/application.properties
View file @
bcdb5ce0
...
...
@@ -55,6 +55,11 @@ cache.enabled = ${KK_CACHE_ENABLED:true}
simText
=
${KK_SIMTEXT:txt,html,htm,asp,jsp,xml,json,properties,md,gitignore,log,java,py,c,cpp,sql,sh,bat,m,bas,prg,cmd}
#多媒体类型,默认如下,可自定义添加
media
=
${KK_MEDIA:mp3,wav,mp4,flv}
#是否开启多媒体类型转视频格式转换,目前可转换视频格式有:avi,mov,wmv,3gp,rm
#请谨慎开启此功能,建议异步调用添加到处理队列,并且增加任务队列处理线程,防止视频转换占用完线程资源,转换比较耗费时间,并且控制了只能串行处理转换任务
media.convert.disable
=
${KK_MEDIA_CONVERT_DISABLE:false}
#支持转换的视频类型
convertMedias
=
${KK_CONVERTMEDIAS:avi,mov,wmv,mkv,3gp,rm}
#office类型文档(word ppt)样式,默认为图片(image),可配置为pdf(预览时也有按钮切换)
office.preview.type
=
${KK_OFFICE_PREVIEW_TYPE:image}
#是否关闭office预览切换开关,默认为false,可配置为true关闭
...
...
server/src/main/java/cn/keking/config/ConfigConstants.java
View file @
bcdb5ce0
...
...
@@ -24,6 +24,8 @@ public class ConfigConstants {
private
static
Boolean
cacheEnabled
;
private
static
String
[]
simTexts
=
{};
private
static
String
[]
medias
=
{};
private
static
String
[]
convertMedias
=
{};
private
static
String
mediaConvertDisable
;
private
static
String
officePreviewType
;
private
static
String
officePreviewSwitchDisabled
;
private
static
String
ftpUsername
;
...
...
@@ -89,6 +91,33 @@ public class ConfigConstants {
ConfigConstants
.
medias
=
Media
;
}
public
static
String
[]
getConvertMedias
()
{
return
convertMedias
;
}
@Value
(
"${convertMedias:avi,mov,wmv,mkv,3gp,rm}"
)
public
void
setConvertMedias
(
String
convertMedia
)
{
String
[]
mediaArr
=
convertMedia
.
split
(
","
);
setConvertMediaValue
(
mediaArr
);
}
public
static
void
setConvertMediaValue
(
String
[]
ConvertMedia
)
{
ConfigConstants
.
convertMedias
=
ConvertMedia
;
}
public
static
String
getMediaConvertDisable
()
{
return
mediaConvertDisable
;
}
@Value
(
"${media.convert.disable:true}"
)
public
void
setMediaConvertDisable
(
String
mediaConvertDisable
)
{
setMediaConvertDisableValue
(
mediaConvertDisable
);
}
public
static
void
setMediaConvertDisableValue
(
String
mediaConvertDisable
)
{
ConfigConstants
.
mediaConvertDisable
=
mediaConvertDisable
;
}
public
static
String
getOfficePreviewType
()
{
return
officePreviewType
;
}
...
...
server/src/main/java/cn/keking/model/FileType.java
View file @
bcdb5ce0
...
...
@@ -33,6 +33,7 @@ public enum FileType {
private
static
final
String
[]
SSIM_TEXT_TYPES
=
ConfigConstants
.
getSimText
();
private
static
final
String
[]
CODES
=
{
"java"
,
"c"
,
"php"
,
"go"
,
"python"
,
"py"
,
"js"
,
"html"
,
"ftl"
,
"css"
,
"lua"
,
"sh"
,
"rb"
,
"yml"
,
"json"
,
"h"
,
"cpp"
,
"cs"
,
"aspx"
,
"jsp"
};
private
static
final
String
[]
MEDIA_TYPES
=
ConfigConstants
.
getMedia
();
public
static
final
String
[]
MEDIA_TYPES_CONVERT
=
ConfigConstants
.
getConvertMedias
();
private
static
final
Map
<
String
,
FileType
>
FILE_TYPE_MAPPER
=
new
HashMap
<>();
static
{
...
...
@@ -51,6 +52,9 @@ public enum FileType {
for
(
String
media
:
MEDIA_TYPES
)
{
FILE_TYPE_MAPPER
.
put
(
media
,
FileType
.
MEDIA
);
}
for
(
String
media
:
MEDIA_TYPES_CONVERT
)
{
FILE_TYPE_MAPPER
.
put
(
media
,
FileType
.
MEDIA
);
}
for
(
String
tif
:
TIFF_TYPES
)
{
FILE_TYPE_MAPPER
.
put
(
tif
,
FileType
.
TIFF
);
}
...
...
server/src/main/java/cn/keking/service/FileHandlerService.java
View file @
bcdb5ce0
...
...
@@ -286,4 +286,27 @@ public class FileHandlerService {
}
return
attribute
;
}
/**
* @return 已转换过的视频文件集合(缓存)
*/
public
Map
<
String
,
String
>
listConvertedMedias
()
{
return
cacheService
.
getMediaConvertCache
();
}
/**
* 添加转换后的视频文件缓存
* @param fileName
* @param value
*/
public
void
addConvertedMedias
(
String
fileName
,
String
value
)
{
cacheService
.
putMediaConvertCache
(
fileName
,
value
);
}
/**
* @return 已转换视频文件缓存,根据文件名获取
*/
public
String
getConvertedMedias
(
String
key
)
{
return
cacheService
.
getMediaConvertCache
(
key
);
}
}
server/src/main/java/cn/keking/service/cache/CacheService.java
View file @
bcdb5ce0
...
...
@@ -12,15 +12,18 @@ public interface CacheService {
String
FILE_PREVIEW_PDF_KEY
=
"converted-preview-pdf-file"
;
String
FILE_PREVIEW_IMGS_KEY
=
"converted-preview-imgs-file"
;
//压缩包内图片文件集合
String
FILE_PREVIEW_PDF_IMGS_KEY
=
"converted-preview-pdfimgs-file"
;
String
FILE_PREVIEW_MEDIA_CONVERT_KEY
=
"converted-preview-media-file"
;
String
TASK_QUEUE_NAME
=
"convert-task"
;
Integer
DEFAULT_PDF_CAPACITY
=
500000
;
Integer
DEFAULT_IMG_CAPACITY
=
500000
;
Integer
DEFAULT_PDFIMG_CAPACITY
=
500000
;
Integer
DEFAULT_MEDIACONVERT_CAPACITY
=
500000
;
void
initPDFCachePool
(
Integer
capacity
);
void
initIMGCachePool
(
Integer
capacity
);
void
initPdfImagesCachePool
(
Integer
capacity
);
void
initMediaConvertCachePool
(
Integer
capacity
);
void
putPDFCache
(
String
key
,
String
value
);
void
putImgCache
(
String
key
,
List
<
String
>
value
);
Map
<
String
,
String
>
getPDFCache
();
...
...
@@ -29,6 +32,9 @@ public interface CacheService {
List
<
String
>
getImgCache
(
String
key
);
Integer
getPdfImageCache
(
String
key
);
void
putPdfImageCache
(
String
pdfFilePath
,
int
num
);
Map
<
String
,
String
>
getMediaConvertCache
();
void
putMediaConvertCache
(
String
key
,
String
value
);
String
getMediaConvertCache
(
String
key
);
void
cleanCache
();
void
addQueueTask
(
String
url
);
String
takeQueueTask
()
throws
InterruptedException
;
...
...
server/src/main/java/cn/keking/service/cache/impl/CacheServiceJDKImpl.java
View file @
bcdb5ce0
...
...
@@ -26,6 +26,7 @@ public class CacheServiceJDKImpl implements CacheService {
private
Map
<
String
,
String
>
pdfCache
;
private
Map
<
String
,
List
<
String
>>
imgCache
;
private
Map
<
String
,
Integer
>
pdfImagesCache
;
private
Map
<
String
,
String
>
mediaConvertCache
;
private
static
final
int
QUEUE_SIZE
=
500000
;
private
final
BlockingQueue
<
String
>
blockingQueue
=
new
ArrayBlockingQueue
<>(
QUEUE_SIZE
);
...
...
@@ -34,6 +35,7 @@ public class CacheServiceJDKImpl implements CacheService {
initPDFCachePool
(
CacheService
.
DEFAULT_PDF_CAPACITY
);
initIMGCachePool
(
CacheService
.
DEFAULT_IMG_CAPACITY
);
initPdfImagesCachePool
(
CacheService
.
DEFAULT_PDFIMG_CAPACITY
);
initMediaConvertCachePool
(
CacheService
.
DEFAULT_MEDIACONVERT_CAPACITY
);
}
@Override
...
...
@@ -79,6 +81,21 @@ public class CacheServiceJDKImpl implements CacheService {
pdfImagesCache
.
put
(
pdfFilePath
,
num
);
}
@Override
public
Map
<
String
,
String
>
getMediaConvertCache
()
{
return
mediaConvertCache
;
}
@Override
public
void
putMediaConvertCache
(
String
key
,
String
value
)
{
mediaConvertCache
.
put
(
key
,
value
);
}
@Override
public
String
getMediaConvertCache
(
String
key
)
{
return
mediaConvertCache
.
get
(
key
);
}
@Override
public
void
cleanCache
()
{
initPDFCachePool
(
CacheService
.
DEFAULT_PDF_CAPACITY
);
...
...
@@ -116,4 +133,12 @@ public class CacheServiceJDKImpl implements CacheService {
.
maximumWeightedCapacity
(
capacity
).
weigher
(
Weighers
.
singleton
())
.
build
();
}
@Override
public
void
initMediaConvertCachePool
(
Integer
capacity
)
{
mediaConvertCache
=
new
ConcurrentLinkedHashMap
.
Builder
<
String
,
String
>()
.
maximumWeightedCapacity
(
capacity
).
weigher
(
Weighers
.
singleton
())
.
build
();
}
}
server/src/main/java/cn/keking/service/cache/impl/CacheServiceRedisImpl.java
View file @
bcdb5ce0
...
...
@@ -34,6 +34,11 @@ public class CacheServiceRedisImpl implements CacheService {
@Override
public
void
initPdfImagesCachePool
(
Integer
capacity
)
{
}
@Override
public
void
initMediaConvertCachePool
(
Integer
capacity
)
{
}
@Override
public
void
putPDFCache
(
String
key
,
String
value
)
{
RMapCache
<
String
,
String
>
convertedList
=
redissonClient
.
getMapCache
(
FILE_PREVIEW_PDF_KEY
);
...
...
@@ -80,6 +85,23 @@ public class CacheServiceRedisImpl implements CacheService {
convertedList
.
fastPut
(
pdfFilePath
,
num
);
}
@Override
public
Map
<
String
,
String
>
getMediaConvertCache
()
{
return
redissonClient
.
getMapCache
(
FILE_PREVIEW_MEDIA_CONVERT_KEY
);
}
@Override
public
void
putMediaConvertCache
(
String
key
,
String
value
)
{
RMapCache
<
String
,
String
>
convertedList
=
redissonClient
.
getMapCache
(
FILE_PREVIEW_MEDIA_CONVERT_KEY
);
convertedList
.
fastPut
(
key
,
value
);
}
@Override
public
String
getMediaConvertCache
(
String
key
)
{
RMapCache
<
String
,
String
>
convertedList
=
redissonClient
.
getMapCache
(
FILE_PREVIEW_MEDIA_CONVERT_KEY
);
return
convertedList
.
get
(
key
);
}
@Override
public
void
cleanCache
()
{
cleanPdfCache
();
...
...
server/src/main/java/cn/keking/service/cache/impl/CacheServiceRocksDBImpl.java
View file @
bcdb5ce0
...
...
@@ -73,6 +73,11 @@ public class CacheServiceRocksDBImpl implements CacheService {
}
@Override
public
void
initMediaConvertCachePool
(
Integer
capacity
)
{
}
@Override
public
void
putPDFCache
(
String
key
,
String
value
)
{
try
{
...
...
@@ -171,6 +176,40 @@ public class CacheServiceRocksDBImpl implements CacheService {
}
}
@Override
public
Map
<
String
,
String
>
getMediaConvertCache
()
{
Map
<
String
,
String
>
result
=
new
HashMap
<>();
try
{
result
=
(
Map
<
String
,
String
>)
toObject
(
db
.
get
(
FILE_PREVIEW_MEDIA_CONVERT_KEY
.
getBytes
()));
}
catch
(
RocksDBException
|
IOException
|
ClassNotFoundException
e
)
{
LOGGER
.
error
(
"Get from RocksDB Exception"
+
e
);
}
return
result
;
}
@Override
public
void
putMediaConvertCache
(
String
key
,
String
value
)
{
try
{
Map
<
String
,
String
>
mediaConvertCacheItem
=
getMediaConvertCache
();
mediaConvertCacheItem
.
put
(
key
,
value
);
db
.
put
(
FILE_PREVIEW_MEDIA_CONVERT_KEY
.
getBytes
(),
toByteArray
(
mediaConvertCacheItem
));
}
catch
(
RocksDBException
|
IOException
e
)
{
LOGGER
.
error
(
"Put into RocksDB Exception"
+
e
);
}
}
@Override
public
String
getMediaConvertCache
(
String
key
)
{
String
result
=
""
;
try
{
Map
<
String
,
String
>
map
=
(
Map
<
String
,
String
>)
toObject
(
db
.
get
(
FILE_PREVIEW_MEDIA_CONVERT_KEY
.
getBytes
()));
result
=
map
.
get
(
key
);
}
catch
(
RocksDBException
|
IOException
|
ClassNotFoundException
e
)
{
LOGGER
.
error
(
"Get from RocksDB Exception"
+
e
);
}
return
result
;
}
@Override
public
void
cleanCache
()
{
try
{
...
...
server/src/main/java/cn/keking/service/impl/MediaFilePreviewImpl.java
View file @
bcdb5ce0
package
cn
.
keking
.
service
.
impl
;
import
cn.keking.config.ConfigConstants
;
import
cn.keking.model.FileAttribute
;
import
cn.keking.model.FileType
;
import
cn.keking.model.ReturnResponse
;
import
cn.keking.service.FilePreview
;
import
cn.keking.utils.DownloadUtils
;
import
cn.keking.service.FileHandlerService
;
import
cn.keking.web.filter.BaseUrlFilter
;
import
org.artofsolving.jodconverter.util.ConfigUtils
;
import
org.bytedeco.ffmpeg.global.avcodec
;
import
org.bytedeco.javacv.FFmpegFrameGrabber
;
import
org.bytedeco.javacv.FFmpegFrameRecorder
;
import
org.bytedeco.javacv.Frame
;
import
org.springframework.stereotype.Service
;
import
org.springframework.ui.Model
;
import
java.io.File
;
/**
* @author : kl
...
...
@@ -21,6 +29,8 @@ public class MediaFilePreviewImpl implements FilePreview {
private
final
FileHandlerService
fileHandlerService
;
private
final
OtherFilePreviewImpl
otherFilePreview
;
private
static
Object
LOCK
=
new
Object
();
public
MediaFilePreviewImpl
(
FileHandlerService
fileHandlerService
,
OtherFilePreviewImpl
otherFilePreview
)
{
this
.
fileHandlerService
=
fileHandlerService
;
this
.
otherFilePreview
=
otherFilePreview
;
...
...
@@ -34,14 +44,123 @@ public class MediaFilePreviewImpl implements FilePreview {
if
(
response
.
isFailure
())
{
return
otherFilePreview
.
notSupportedFile
(
model
,
fileAttribute
,
response
.
getMsg
());
}
else
{
model
.
addAttribute
(
"mediaUrl"
,
BaseUrlFilter
.
getBaseUrl
()
+
fileHandlerService
.
getRelativePath
(
response
.
getContent
()));
url
=
BaseUrlFilter
.
getBaseUrl
()
+
fileHandlerService
.
getRelativePath
(
response
.
getContent
());
fileAttribute
.
setUrl
(
url
);
}
}
else
{
}
if
(
checkNeedConvert
(
fileAttribute
.
getSuffix
())){
url
=
convertUrl
(
fileAttribute
);
}
else
{
//正常media类型
String
[]
medias
=
ConfigConstants
.
getMedia
();
for
(
String
media:
medias
){
if
(
media
.
equals
(
fileAttribute
.
getSuffix
())){
model
.
addAttribute
(
"mediaUrl"
,
url
);
return
MEDIA_FILE_PREVIEW_PAGE
;
}
}
return
otherFilePreview
.
notSupportedFile
(
model
,
fileAttribute
,
"暂不支持"
);
}
model
.
addAttribute
(
"mediaUrl"
,
url
);
return
MEDIA_FILE_PREVIEW_PAGE
;
}
/**
* 检查视频文件处理逻辑
* 返回处理过后的url
* @return url
*/
private
String
convertUrl
(
FileAttribute
fileAttribute
)
{
String
url
=
fileAttribute
.
getUrl
();
if
(
fileHandlerService
.
listConvertedMedias
().
containsKey
(
url
)){
url
=
fileHandlerService
.
getConvertedMedias
(
url
);
}
else
{
if
(!
fileHandlerService
.
listConvertedMedias
().
containsKey
(
url
)){
synchronized
(
LOCK
){
if
(!
fileHandlerService
.
listConvertedMedias
().
containsKey
(
url
)){
String
convertedUrl
=
convertToMp4
(
fileAttribute
);
//加入缓存
fileHandlerService
.
addConvertedMedias
(
url
,
convertedUrl
);
url
=
convertedUrl
;
}
}
}
}
return
url
;
}
/**
* 检查视频文件转换是否已开启,以及当前文件是否需要转换
* @return
*/
private
boolean
checkNeedConvert
(
String
suffix
)
{
//1.检查开关是否开启
if
(
"false"
.
equals
(
ConfigConstants
.
getMediaConvertDisable
())){
return
false
;
}
//2.检查当前文件是否需要转换
String
[]
mediaTypesConvert
=
FileType
.
MEDIA_TYPES_CONVERT
;
String
type
=
suffix
;
for
(
String
temp
:
mediaTypesConvert
){
if
(
type
.
equals
(
temp
)){
return
true
;
}
}
return
false
;
}
/**
* 将浏览器不兼容视频格式转换成MP4
* @param fileAttribute
* @return
*/
private
static
String
convertToMp4
(
FileAttribute
fileAttribute
)
{
//说明:这里做临时处理,取上传文件的目录
String
homePath
=
ConfigUtils
.
getHomePath
();
String
filePath
=
homePath
+
File
.
separator
+
"file"
+
File
.
separator
+
"demo"
+
File
.
separator
+
fileAttribute
.
getName
();
String
convertFileName
=
fileAttribute
.
getUrl
().
replace
(
fileAttribute
.
getSuffix
(),
"mp4"
);
File
file
=
new
File
(
filePath
);
FFmpegFrameGrabber
frameGrabber
=
new
FFmpegFrameGrabber
(
file
);
String
fileName
=
null
;
Frame
captured_frame
=
null
;
FFmpegFrameRecorder
recorder
=
null
;
try
{
fileName
=
file
.
getAbsolutePath
().
replace
(
fileAttribute
.
getSuffix
(),
"mp4"
);
File
desFile
=
new
File
(
fileName
);
//判断一下防止穿透缓存
if
(
desFile
.
exists
()){
return
fileName
;
}
frameGrabber
.
start
();
recorder
=
new
FFmpegFrameRecorder
(
fileName
,
frameGrabber
.
getImageWidth
(),
frameGrabber
.
getImageHeight
(),
frameGrabber
.
getAudioChannels
());
recorder
.
setVideoCodec
(
avcodec
.
AV_CODEC_ID_H264
);
//avcodec.AV_CODEC_ID_H264 //AV_CODEC_ID_MPEG4
recorder
.
setFormat
(
"mp4"
);
recorder
.
setFrameRate
(
frameGrabber
.
getFrameRate
());
//recorder.setSampleFormat(frameGrabber.getSampleFormat()); //
recorder
.
setSampleRate
(
frameGrabber
.
getSampleRate
());
recorder
.
setAudioChannels
(
frameGrabber
.
getAudioChannels
());
recorder
.
setFrameRate
(
frameGrabber
.
getFrameRate
());
recorder
.
start
();
while
((
captured_frame
=
frameGrabber
.
grabFrame
())
!=
null
)
{
try
{
recorder
.
setTimestamp
(
frameGrabber
.
getTimestamp
());
recorder
.
record
(
captured_frame
);
}
catch
(
Exception
e
)
{
}
}
recorder
.
stop
();
recorder
.
release
();
frameGrabber
.
stop
();
}
catch
(
Exception
e
)
{
e
.
printStackTrace
();
}
//是否删除源文件
//file.delete();
return
convertFileName
;
}
}
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