引言

大家好!今天给大家带来一个超实用的技术教程组合拳——如何在阿里云ModelScope免费服务器上搭建文件上传服务,并通过ngrok实现内网穿透,让外网也能访问你的服务。全程保姆级教学,跟着操作就能成功!


一、领取ModelScope免费服务器

1. 什么是ModelScope Notebook?

ModelScope Notebook是阿里云提供的云端机器学习开发环境,支持Python编程,实名用户可获得免费算力额度,完美解决本地算力不足的问题。

2. 领取步骤

第一步:登录注册

  • 访问 魔塔社区

  • 注册账号并完成实名认证

第二步:进入Notebook

  • 点击右上角「控制台」

  • 选择「Notebook」进入管理页面

第三步:创建实例

  • 点击「新建Notebook」

  • 选择GPU环境(36小时免费额度)或CPU环境(长期免费使用)

  • 等待实例启动(约1-2分钟)

    图片


二、搭建文件上传服务

1. 项目结构

如果选择CPU环境,若超过1小时无操作将触发自动关闭功能,重启后会清空/mnt/workspace之外的数据,所有为了持久化,将数据放在/mnt/workspace下。

首先创建项目目录结构:

mkdir
 -p /mnt/workspace/py_file/{templates,uploads,chunks}

cd
 py_file

2. 创建Flask服务端代码

创建 app.py 文件:

from
 flask 
import
 Flask, render_template, request, redirect, url_for, jsonify

import
 os

import
 uuid

from
 datetime 
import
 datetime


app = Flask(__name__)

app.config[
'UPLOAD_FOLDER'
] = 
'uploads'

app.config[
'CHUNK_FOLDER'
] = 
'chunks'

app.config[
'MAX_CONTENT_LENGTH'
] = 
2
 * 
1024
 * 
1024
 * 
1024
# 2GB

app.config[
'CHUNK_SIZE'
] = 
5
 * 
1024
 * 
1024
# 5MB per chunk


ALLOWED_EXTENSIONS = {
'txt'
, 
'pdf'
, 
'png'
, 
'jpg'
, 
'jpeg'
, 
'gif'
, 
'zip'
, 
'rar'
, 

                      
'doc'
, 
'docx'
, 
'xls'
, 
'xlsx'
, 
'mp4'
, 
'avi'
, 
'mov'
, 
'mkv'
}


def
allowed_file
(
filename
):

    
return
'.'
in
 filename 
and
 filename.rsplit(
'.'
, 
1
)[
1
].lower() 
in
 ALLOWED_EXTENSIONS


@app.route('/', methods=['GET'])

def
index
():

    
return
 render_template(
'index.html'
)


@app.route('/api/upload/init', methods=['POST'])

def
init_upload
():

    data = request.get_json()

    filename = data.get(
'filename'
)

    total_size = data.get(
'totalSize'
)

    

    
if
not
 filename 
or
not
 allowed_file(filename):

        
return
 jsonify({
'error'
: 
'不允许的文件类型'
}), 
400

    

    upload_id = 
str
(uuid.uuid4())

    chunk_dir = os.path.join(app.config[
'CHUNK_FOLDER'
], upload_id)

    os.makedirs(chunk_dir, exist_ok=
True
)

    

    
return
 jsonify({

        
'uploadId'
: upload_id,

        
'chunkSize'
: app.config[
'CHUNK_SIZE'
],

        
'resumed'
: 
False

    })


@app.route('/api/upload/chunk', methods=['POST'])

def
upload_chunk
():

    upload_id = request.form.get(
'uploadId'
)

    chunk_index = 
int
(request.form.get(
'chunkIndex'
))

    file = request.files.get(
'file'
)

    

    
if
not
 upload_id 
or
not
 file:

        
return
 jsonify({
'error'
: 
'缺少参数'
}), 
400

    

    chunk_dir = os.path.join(app.config[
'CHUNK_FOLDER'
], upload_id)

    chunk_path = os.path.join(chunk_dir, 
f'chunk_{chunk_index}'
)

    file.save(chunk_path)

    

    
return
 jsonify({
'chunkIndex'
: chunk_index, 
'status'
: 
'success'
})


@app.route('/api/upload/complete', methods=['POST'])

def
complete_upload
():

    data = request.get_json()

    upload_id = data.get(
'uploadId'
)

    filename = data.get(
'filename'
)

    

    chunk_dir = os.path.join(app.config[
'CHUNK_FOLDER'
], upload_id)

    timestamp = datetime.now().strftime(
'%Y%m%d_%H%M%S_'
)

    final_filename = timestamp + filename

    final_path = os.path.join(app.config[
'UPLOAD_FOLDER'
], final_filename)

    

    
with
open
(final_path, 
'wb'
) 
as
 final_file:

        chunk_files = 
sorted
([f 
for
 f 
in
 os.listdir(chunk_dir) 
if
 f.startswith(
'chunk_'
)],

                           key=
lambda
 x: 
int
(x.split(
'_'
)[
1
]))

        
for
 chunk_file 
in
 chunk_files:

            chunk_path = os.path.join(chunk_dir, chunk_file)

            
with
open
(chunk_path, 
'rb'
) 
as
 cf:

                final_file.write(cf.read())

    

    
import
 shutil

    shutil.rmtree(chunk_dir)

    

    
return
 jsonify({

        
'filename'
: filename,

        
'savedName'
: final_filename,

        
'size'
: os.path.getsize(final_path),

        
'uploadTime'
: datetime.now().strftime(
'%Y-%m-%d %H:%M:%S'
)

    })


@app.route('/list')

def
list_files
():

    files = []

    
if
 os.path.exists(app.config[
'UPLOAD_FOLDER'
]):

        
for
 filename 
in
 os.listdir(app.config[
'UPLOAD_FOLDER'
]):

            file_path = os.path.join(app.config[
'UPLOAD_FOLDER'
], filename)

            
if
 os.path.isfile(file_path):

                files.append({

                    
'name'
: filename,

                    
'size'
: os.path.getsize(file_path),

                    
'mtime'
: datetime.fromtimestamp(os.path.getmtime(file_path)).strftime(
'%Y-%m-%d %H:%M:%S'
)

                })

    files.sort(key=
lambda
 x: x[
'mtime'
], reverse=
True
)

    
return
 render_template(
'list.html'
, files=files)


if
 __name__ == 
'__main__'
:

    os.makedirs(app.config[
'UPLOAD_FOLDER'
], exist_ok=
True
)

    os.makedirs(app.config[
'CHUNK_FOLDER'
], exist_ok=
True
)

    app.run(debug=
True
, host=
'0.0.0.0'
, port=
5000
)

3. 创建前端页面

创建 templates/index.html

<!DOCTYPE html>

<
html
 
lang
=
"zh-CN"
>

<
head
>

    
<
meta
 
charset
=
"UTF-8"
>

    
<
title
>
文件上传
</
title
>

    
<
style
>

        
body
 { 
font-family
: -apple-system, BlinkMacSystemFont, 
'Segoe UI'
, Roboto, sans-serif; 
padding
: 
20px
; 
max-width
: 
600px
; 
margin
: 
0
 auto; }

        
.upload-area
 { 
border
: 
2px
 dashed 
#ccc
; 
border-radius
: 
10px
; 
padding
: 
40px
; 
text-align
: center; 
cursor
: pointer; }

        
.upload-area
:hover
 { 
border-color
: 
#007bff
; }

        
#fileInput
 { 
display
: none; }

        
.progress-bar
 { 
width
: 
100%
; 
height
: 
20px
; 
background
: 
#eee
; 
border-radius
: 
10px
; 
margin-top
: 
20px
; }

        
.progress
 { 
height
: 
100%
; 
background
: 
#007bff
; 
border-radius
: 
10px
; 
transition
: width 
0.3s
; }

    
</
style
>

</
head
>

<
body
>

    
<
h1
>
📤 文件上传
</
h1
>

    
<
div
 
class
=
"upload-area"
 
id
=
"uploadArea"
>

        
<
p
>
点击或拖拽文件到此处上传
</
p
>

        
<
p
 
style
=
"color: #666; font-size: 14px;"
>
支持大文件分片上传(最大2GB)
</
p
>

    
</
div
>

    
<
input
 
type
=
"file"
 
id
=
"fileInput"
>

    
<
div
 
class
=
"progress-bar"
 
id
=
"progressBar"
 
style
=
"display: none;"
>

        
<
div
 
class
=
"progress"
 
id
=
"progress"
>
</
div
>

    
</
div
>

    
<
p
 
id
=
"status"
>
</
p
>


    
<
script
>

        
const
 uploadArea = 
document
.
getElementById
(
'uploadArea'
);

        
const
 fileInput = 
document
.
getElementById
(
'fileInput'
);

        
const
 progressBar = 
document
.
getElementById
(
'progressBar'
);

        
const
 progress = 
document
.
getElementById
(
'progress'
);

        
const
 status = 
document
.
getElementById
(
'status'
);


        uploadArea.
addEventListener
(
'click'
, 
() =>
 fileInput.
click
());

        uploadArea.
addEventListener
(
'dragover'
, 
(
e
) =>
 e.
preventDefault
());

        uploadArea.
addEventListener
(
'drop'
, 
(
e
) =>
 {

            e.
preventDefault
();

            
const
 files = e.
dataTransfer
.
files
;

            
if
 (files.
length
 > 
0
) 
uploadFile
(files[
0
]);

        });

        fileInput.
addEventListener
(
'change'
, 
(
e
) =>
uploadFile
(e.
target
.
files
[
0
]));


        
async
function
uploadFile
(
file
) {

            
if
 (!file) 
return
;

            

            
const
CHUNK_SIZE
 = 
5
 * 
1024
 * 
1024
; 
// 5MB

            
const
 totalChunks = 
Math
.
ceil
(file.
size
 / 
CHUNK_SIZE
);

            
let
 uploadedChunks = 
0
;


            progressBar.
style
.
display
 = 
'block'
;

            status.
textContent
 = 
`初始化上传...`
;


            
const
 initResponse = 
await
fetch
(
'/api/upload/init'
, {

                
method
: 
'POST'
,

                
headers
: { 
'Content-Type'
: 
'application/json'
 },

                
body
: 
JSON
.
stringify
({ 
filename
: file.
name
, 
totalSize
: file.
size
 })

            });

            
const
 { uploadId, chunkSize } = 
await
 initResponse.
json
();


            
for
 (
let
 i = 
0
; i < totalChunks; i++) {

                
const
 start = i * chunkSize;

                
const
 end = 
Math
.
min
(start + chunkSize, file.
size
);

                
const
 chunk = file.
slice
(start, end);


                
const
 formData = 
new
FormData
();

                formData.
append
(
'uploadId'
, uploadId);

                formData.
append
(
'chunkIndex'
, i);

                formData.
append
(
'totalChunks'
, totalChunks);

                formData.
append
(
'file'
, chunk);


                
await
fetch
(
'/api/upload/chunk'
, { 
method
: 
'POST'
, 
body
: formData });

                

                uploadedChunks++;

                progress.
style
.
width
 = 
`${(uploadedChunks / totalChunks) * 100}%`
;

                status.
textContent
 = 
`上传中: ${Math.round((uploadedChunks / totalChunks) * 100)}%`
;

            }


            
const
 completeResponse = 
await
fetch
(
'/api/upload/complete'
, {

                
method
: 
'POST'
,

                
headers
: { 
'Content-Type'
: 
'application/json'
 },

                
body
: 
JSON
.
stringify
({ uploadId, 
filename
: file.
name
 })

            });

            
const
 result = 
await
 completeResponse.
json
();

            

            status.
textContent
 = 
`✅ 上传成功!文件名: ${result.filename}, 大小: ${formatSize(result.size)}`
;

        }


        
function
formatSize
(
bytes
) {

            
if
 (bytes < 
1024
) 
return
 bytes + 
' B'
;

            
if
 (bytes < 
1024
 * 
1024
) 
return
 (bytes / 
1024
).
toFixed
(
2
) + 
' KB'
;

            
if
 (bytes < 
1024
 * 
1024
 * 
1024
) 
return
 (bytes / (
1024
 * 
1024
)).
toFixed
(
2
) + 
' MB'
;

            
return
 (bytes / (
1024
 * 
1024
 * 
1024
)).
toFixed
(
2
) + 
' GB'
;

        }

    
</
script
>

</
body
>

</
html
>

4. 创建文件列表页面

创建 templates/list.html

<!DOCTYPE html>

<
html
 
lang
=
"zh-CN"
>

<
head
>

    
<
meta
 
charset
=
"UTF-8"
>

    
<
meta
 
name
=
"viewport"
 
content
=
"width=device-width, initial-scale=1.0"
>

    
<
title
>
已上传文件列表
</
title
>

    
<
style
>

        * {

            
margin
: 
0
;

            
padding
: 
0
;

            
box-sizing
: border-box;

        }

        
body
 {

            
font-family
: 
'Segoe UI'
, Tahoma, Geneva, Verdana, sans-serif;

            
background
: 
linear-gradient
(
135deg
, 
#667eea
0%
, 
#764ba2
100%
);

            
min-height
: 
100vh
;

            
padding
: 
40px
20px
;

        }

        
.container
 {

            
background
: white;

            
border-radius
: 
20px
;

            
padding
: 
30px
;

            
box-shadow
: 
0
20px
60px
rgba
(
0
, 
0
, 
0
, 
0.15
);

            
max-width
: 
800px
;

            
margin
: 
0
 auto;

        }

        
.header
 {

            
display
: flex;

            
justify-content
: space-between;

            
align-items
: center;

            
margin-bottom
: 
30px
;

        }

        
h1
 {

            
color
: 
#333
;

            
font-size
: 
28px
;

            
display
: flex;

            
align-items
: center;

            
gap
: 
10px
;

        }

        
.btn
 {

            
padding
: 
10px
25px
;

            
border
: none;

            
border-radius
: 
8px
;

            
font-size
: 
15px
;

            
font-weight
: 
600
;

            
cursor
: pointer;

            
transition
: all 
0.3s
 ease;

            
text-decoration
: none;

            
display
: inline-flex;

            
align-items
: center;

            
gap
: 
8px
;

        }

        
.btn-primary
 {

            
background
: 
linear-gradient
(
135deg
, 
#667eea
0%
, 
#764ba2
100%
);

            
color
: white;

        }

        
.btn-primary
:hover
 {

            
transform
: 
translateY
(-
2px
);

            
box-shadow
: 
0
5px
20px
rgba
(
102
, 
126
, 
234
, 
0.4
);

        }

        
.btn-danger
 {

            
background
: 
#ff4757
;

            
color
: white;

            
padding
: 
10px
20px
;

            
font-size
: 
15px
;

        }

        
.btn-danger
:hover
 {

            
background
: 
#ff3838
;

        }

        
.file-list
 {

            
list-style
: none;

        }

        
.file-item
 {

            
display
: flex;

            
align-items
: center;

            
padding
: 
15px
20px
;

            
border
: 
1px
 solid 
#eee
;

            
border-radius
: 
10px
;

            
margin-bottom
: 
12px
;

            
transition
: all 
0.3s
 ease;

        }

        
.file-item
:hover
 {

            
border-color
: 
#667eea
;

            
background
: 
#f8f9ff
;

        }

        
.file-icon
 {

            
font-size
: 
36px
;

            
margin-right
: 
15px
;

        }

        
.file-details
 {

            
flex
: 
1
;

        }

        
.file-name
 {

            
font-weight
: 
600
;

            
color
: 
#333
;

            
margin-bottom
: 
5px
;

        }

        
.file-meta
 {

            
font-size
: 
13px
;

            
color
: 
#999
;

        }

        
.file-actions
 {

            
display
: flex;

            
gap
: 
10px
;

        }

        
.no-files
 {

            
text-align
: center;

            
padding
: 
60px
20px
;

            
color
: 
#999
;

        }

        
.no-files-icon
 {

            
font-size
: 
60px
;

            
margin-bottom
: 
20px
;

        }

        
.no-files
p
 {

            
font-size
: 
16px
;

        }

    
</
style
>

</
head
>

<
body
>

    
<
div
 
class
=
"container"
>

        
<
div
 
class
=
"header"
>

            
<
h1
>
📁 已上传文件列表
</
h1
>

            
<
a
 
href
=
"/"
 
class
=
"btn btn-primary"
>
📤 上传新文件
</
a
>

        
</
div
>

        

        {% if files %}

            
<
ul
 
class
=
"file-list"
>

                {% for file in files %}

                    
<
li
 
class
=
"file-item"
>

                        
<
div
 
class
=
"file-icon"
>
{{ file.icon }}
</
div
>

                        
<
div
 
class
=
"file-details"
>

                            
<
div
 
class
=
"file-name"
>
{{ file.name }}
</
div
>

                            
<
div
 
class
=
"file-meta"
>
{{ file.size }} | {{ file.modified }}
</
div
>

                        
</
div
>

                        
<
div
 
class
=
"file-actions"
>

                            
<
a
 
href
=
"/download/{{ file.name }}"
 
class
=
"btn btn-primary"
>
下载
</
a
>

                            
<
form
 
action
=
"/delete/{{ file.name }}"
 
method
=
"post"
 
style
=
"display: inline;"
>

                                
<
button
 
type
=
"submit"
 
class
=
"btn btn-danger"
 
onclick
=
"return confirm('确定要删除此文件吗?')"
>
删除
</
button
>

                            
</
form
>

                        
</
div
>

                    
</
li
>

                {% endfor %}

            
</
ul
>

        {% else %}

            
<
div
 
class
=
"no-files"
>

                
<
div
 
class
=
"no-files-icon"
>
📂
</
div
>

                
<
p
>
暂无上传的文件
</
p
>

            
</
div
>

        {% endif %}

    
</
div
>

</
body
>

</
html
>

5. 创建上传成功页面

创建 templates/uploaded.html

<!DOCTYPE html>

<
html
 
lang
=
"zh-CN"
>

<
head
>

    
<
meta
 
charset
=
"UTF-8"
>

    
<
meta
 
name
=
"viewport"
 
content
=
"width=device-width, initial-scale=1.0"
>

    
<
title
>
上传成功
</
title
>

    
<
style
>

        * {

            
margin
: 
0
;

            
padding
: 
0
;

            
box-sizing
: border-box;

        }

        
body
 {

            
font-family
: 
'Segoe UI'
, Tahoma, Geneva, Verdana, sans-serif;

            
background
: 
linear-gradient
(
135deg
, 
#667eea
0%
, 
#764ba2
100%
);

            
min-height
: 
100vh
;

            
display
: flex;

            
justify-content
: center;

            
align-items
: center;

            
padding
: 
20px
;

        }

        
.container
 {

            
background
: white;

            
border-radius
: 
20px
;

            
padding
: 
40px
;

            
text-align
: center;

            
box-shadow
: 
0
20px
60px
rgba
(
0
, 
0
, 
0
, 
0.15
);

            
max-width
: 
450px
;

            
width
: 
100%
;

        }

        
.success-icon
 {

            
font-size
: 
80px
;

            
margin-bottom
: 
20px
;

            
animation
: bounce 
0.6s
 ease;

        }

        
@keyframes
 bounce {

            
0%
, 
100%
 { 
transform
: 
scale
(
1
); }

            
50%
 { 
transform
: 
scale
(
1.1
); }

        }

        
h1
 {

            
color
: 
#333
;

            
margin-bottom
: 
15px
;

            
font-size
: 
28px
;

        }

        
p
 {

            
color
: 
#666
;

            
margin-bottom
: 
30px
;

            
font-size
: 
16px
;

        }

        
.file-info
 {

            
background
: 
#f8f9fa
;

            
padding
: 
20px
;

            
border-radius
: 
12px
;

            
margin-bottom
: 
30px
;

            
text-align
: left;

        }

        
.file-info
div
 {

            
margin-bottom
: 
10px
;

        }

        
.file-info
div
:last-child
 {

            
margin-bottom
: 
0
;

        }

        
.file-info
strong
 {

            
color
: 
#333
;

            
min-width
: 
100px
;

            
display
: inline-block;

        }

        
.btn
 {

            
padding
: 
12px
30px
;

            
border
: none;

            
border-radius
: 
8px
;

            
font-size
: 
16px
;

            
font-weight
: 
600
;

            
cursor
: pointer;

            
transition
: all 
0.3s
 ease;

            
text-decoration
: none;

            
display
: inline-block;

        }

        
.btn-primary
 {

            
background
: 
linear-gradient
(
135deg
, 
#667eea
0%
, 
#764ba2
100%
);

            
color
: white;

        }

        
.btn-primary
:hover
 {

            
transform
: 
translateY
(-
2px
);

            
box-shadow
: 
0
5px
20px
rgba
(
102
, 
126
, 
234
, 
0.4
);

        }

        
.btn-secondary
 {

            
background
: 
#f0f0f0
;

            
color
: 
#666
;

            
margin-left
: 
10px
;

        }

        
.btn-secondary
:hover
 {

            
background
: 
#e0e0e0
;

        }

    
</
style
>

</
head
>

<
body
>

    
<
div
 
class
=
"container"
>

        
<
div
 
class
=
"success-icon"
></
div
>

        
<
h1
>
上传成功!
</
h1
>

        
<
p
>
您的文件已成功上传到服务器
</
p
>

        

        
<
div
 
class
=
"file-info"
>

            
<
div
>
<
strong
>
文件名:
</
strong
>
{{ file_info.filename }}
</
div
>

            
<
div
>
<
strong
>
文件大小:
</
strong
>
{{ file_info.size|format_size }}
</
div
>

            
<
div
>
<
strong
>
上传时间:
</
strong
>
{{ file_info.upload_time }}
</
div
>

        
</
div
>

        

        
<
a
 
href
=
"/"
 
class
=
"btn btn-primary"
>
继续上传
</
a
>

        
<
a
 
href
=
"/list"
 
class
=
"btn btn-secondary"
>
查看文件列表
</
a
>

    
</
div
>

</
body
>

</
html
>

图片

6. 安装依赖并启动服务

# 安装Flask

pip install flask


# 启动服务

python3 app.py

图片

三、什么是ngrok?

ngrok简介

ngrok是一个内网穿透工具,可以将你的本地服务器暴露到公网,让外网用户也能访问。它的核心功能:

  • 🚀 将本地端口映射到公网URL

  • 🔒 支持HTTPS加密连接

  • 📊 提供请求日志和流量统计

  • 🔄 支持多种协议(HTTP/HTTPS/TCP)

为什么需要ngrok?

  • 开发测试:让远程同事测试你的本地服务

  • 演示展示:向客户展示本地开发的应用

  • 调试Webhook:接收第三方服务的回调


四、使用ngrok开启代理

1. 安装ngrok

方法一:下载安装(推荐)

# 下载ngrok

wget https://bin.ngrok.com/c/bNyj1mQVY4c/ngrok-v3-stable-linux-amd64.tgz


# 解压

tar -xzf ngrok-v3-stable-linux-amd64.tgz


# 移动到系统目录

sudo
 
mv
 ngrok /usr/local/bin/

方法二:使用npm安装

npm install -g ngrok

2. 注册并获取Auth Token

  • 访问 ngrok官网 注册账号

  • 登录后在「Your Authtoken」页面复制你的token

3. 配置Auth Token

ngrok config add-authtoken YOUR_AUTH_TOKEN

4. 启动ngrok代理

# 将本地5000端口映射到公网

ngrok http 5000

执行后会看到类似如下输出:

Session Status                online

Account                       YourName (Plan: Free)

Version                       3.1.0

Region                        Asia Pacific (ap)

Latency                       12ms

Web Interface                 http://127.0.0.1:4040

Forwarding                    https://abc123.ngrok.io -> http://localhost:5000

Forwarding                    http://abc123.ngrok.io -> http://localhost:5000

✨ 重点https://abc123.ngrok.io 就是你的公网访问地址!


图片

五、上传文件测试

1. 访问上传页面

打开浏览器,访问ngrok提供的公网URL(如 https://abc123.ngrok.io

图片

2. 上传文件

  • 点击上传区域或拖拽文件到页面

  • 等待上传完成(大文件会自动分片上传,支持断点续传)

  • 查看上传结果

3. 查看已上传文件

访问 https://abc123.ngrok.io/list 查看所有已上传的文件列表


六、常见问题排查

Q1: ngrok启动失败?

解决方案

  • 检查网络连接是否正常

  • 确保Auth Token配置正确

  • 尝试更换端口:ngrok http 8080

Q2: 文件上传失败?

解决方案

  • 检查文件大小是否超过2GB限制

  • 确保uploads和chunks目录有写入权限

  • 查看Flask控制台错误日志

Q3: ngrok URL失效?

解决方案

  • 免费版ngrok每次启动会生成新URL

  • 升级到付费版可获得固定域名

  • 保持ngrok终端窗口打开


总结

今天我们完成了一个完整的技术链路:

  1. 🎁 领取免费服务器:ModelScope Notebook提供免费GPU算力

  2. 🚀 搭建上传服务:使用Flask实现大文件分片上传

  3. 🌐 内网穿透:通过ngrok将本地服务暴露到公网

  4. 📤 测试上传:成功上传文件到服务器

这套组合拳可以应用在很多场景:

  • 临时文件分享服务

  • 开发测试环境

  • 个人项目展示

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐