PCE版本SAP链接SFTP文件服务器

开发板推荐:天空星STM32F407VET6开发板

超高性价比 STM32主控 | 超高主频 | 一板兼容百芯 | 比赛神器 | 沉金彩色丝印

在 SAP S/4HANA PCE (Private Cloud Edition / 私有云版本) 环境中,直接在 ABAP 层实现纯正的 SFTP (SSH File Transfer Protocol) 会面临网络白名单限制和内核不支持的双重物理墙。PCE 是由 SAP 原厂托管的封闭环境,默认禁止一切未经审批的外部端口访问,且剥夺了底层操作系统的 Shell 执行权限。

方案一:引入 SAP BTP Integration Suite (CPI) —— 官方强烈推荐 (Best Practice)
不要让核心 ERP 去做底层的文件传输。

流程:ABAP 将数据通过 OData API、Proxy (SOAP) 或标准的 HTTP 接口发送给 BTP 平台上的 CPI (Cloud Platform Integration)。

优势:CPI 自带标准且极其强大的 SFTP Adapter。CPI 负责存放私钥、密码,并与外部 SFTP 服务器建立安全的 SSH 隧道进行文件上传/下载。

架构收益:完全解耦,S/4HANA PCE 零定制代码,符合 SAP Clean Core 战略。

案二:让对方服务器提供 HTTPS / REST API 接口 (Modern Approach)
如果只有 ABAP 可以用,请让接收端将 SFTP 改造为 RESTful API (HTTPS)。

流程:在 S/4HANA SM59 中配置 Type G (HTTP Connection to External Server) 的 RFC。ABAP 通过 IF_HTTP_CLIENT (或现代的 cl_web_http_client_manager) 将文件转换为 Base64 或 XSTRING,通过 POST 请求直接发送。

优势:PCE 对 HTTPS (443) 端口的支持和打通极其容易,且完全属于标准的 ABAP Cloud 技术栈。

一个简单的示例上传文件
首先创建一个类方法

CLASS zcl_sftp_via_rest DEFINITION
  PUBLIC
  FINAL
  CREATE PUBLIC.

  PUBLIC SECTION.
* 1. 上传文件:传入文件名、二进制数据、目标目录
    METHODS upload_file
      IMPORTING
        iv_filename   TYPE string
        iv_binary     TYPE xstring
        iv_remote_dir TYPE string
      RETURNING
        VALUE(rv_ok)  TYPE abap_bool.

* 2. 下载文件:传入文件名、目标目录,导出二进制数据
    METHODS download_file
      IMPORTING
        iv_filename   TYPE string
        iv_remote_dir TYPE string
      EXPORTING
        ev_binary     TYPE xstring
      RETURNING
        VALUE(rv_ok)  TYPE abap_bool.

* 3. 删除文件:传入文件名、目标目录
    METHODS delete_file
      IMPORTING
        iv_filename   TYPE string
        iv_remote_dir TYPE string
      RETURNING
        VALUE(rv_ok)  TYPE abap_bool.

  PROTECTED SECTION.

  PRIVATE SECTION.
* 内部统一 HTTP 调用引擎,实现代码高度复用 (Clean ABAP)
    METHODS _call_rest_api
      IMPORTING
        iv_uri           TYPE string
        iv_payload       TYPE string
      EXPORTING
        ev_response_body TYPE string
      RETURNING
        VALUE(rv_status) TYPE i.

ENDCLASS.


CLASS zcl_sftp_via_rest IMPLEMENTATION.

  METHOD upload_file.
*----------------------------------------------------------------------*
* [业务方法] 1. 上传文件
*----------------------------------------------------------------------*
    rv_ok = abap_false.

    TYPES: BEGIN OF ty_request,
             filename       TYPE string,
             content_base64 TYPE string,
             remote_dir     TYPE string,
           END OF ty_request.

    DATA: ls_request TYPE ty_request.

    ls_request-filename       = iv_filename.
    ls_request-remote_dir     = iv_remote_dir.
    ls_request-content_base64 = cl_http_utility=>encode_x_base64( iv_binary ).

* 强制转为小写以适配 Python
    DATA(lv_json_payload) = /ui2/cl_json=>serialize(
                              data        = ls_request
                              pretty_name = /ui2/cl_json=>pretty_mode-low_case ).

* 提前声明接收变量,规避旧版 SAP 内联声明语法冲突
    DATA: lv_response TYPE string.

* 调用统一 HTTP 引擎
    DATA(lv_status) = _call_rest_api(
      EXPORTING iv_uri           = '/api/sftp/upload'
                iv_payload       = lv_json_payload
      IMPORTING ev_response_body = lv_response ).

    IF lv_status = 200.
      rv_ok = abap_true.
    ELSE.
      MESSAGE |上传失败[{ lv_status }]: { lv_response }| TYPE 'E'.
    ENDIF.
  ENDMETHOD.


  METHOD download_file.
*----------------------------------------------------------------------*
* [业务方法] 2. 下载文件
*----------------------------------------------------------------------*
    rv_ok = abap_false.
    CLEAR ev_binary.

    TYPES: BEGIN OF ty_request,
             filename   TYPE string,
             remote_dir TYPE string,
           END OF ty_request.

    TYPES: BEGIN OF ty_response,
             status         TYPE string,
             message        TYPE string,
             content_base64 TYPE string,
           END OF ty_response.

    DATA: ls_request  TYPE ty_request,
          ls_response TYPE ty_response.

    ls_request-filename   = iv_filename.
    ls_request-remote_dir = iv_remote_dir.

    DATA(lv_json_payload) = /ui2/cl_json=>serialize(
                              data        = ls_request
                              pretty_name = /ui2/cl_json=>pretty_mode-low_case ).

* 提前声明接收变量
    DATA: lv_response_json TYPE string.

* 调用统一 HTTP 引擎
    DATA(lv_status) = _call_rest_api(
      EXPORTING iv_uri           = '/api/sftp/download'
                iv_payload       = lv_json_payload
      IMPORTING ev_response_body = lv_response_json ).

    IF lv_status = 200.
* 解析响应 JSON 并将小写字段映射回 ABAP
      /ui2/cl_json=>deserialize(
        EXPORTING json        = lv_response_json
                  pretty_name = /ui2/cl_json=>pretty_mode-low_case
        CHANGING  data        = ls_response ).

* 将 Base64 解码为 XSTRING
      ev_binary = cl_http_utility=>decode_x_base64( ls_response-content_base64 ).
      rv_ok = abap_true.
    ELSE.
      MESSAGE |下载失败[{ lv_status }]: { lv_response_json }| TYPE 'E'.
    ENDIF.
  ENDMETHOD.


  METHOD delete_file.
*----------------------------------------------------------------------*
* [业务方法] 3. 删除文件
*----------------------------------------------------------------------*
    rv_ok = abap_false.

    TYPES: BEGIN OF ty_request,
             filename   TYPE string,
             remote_dir TYPE string,
           END OF ty_request.

    DATA(ls_request) = VALUE ty_request( filename = iv_filename remote_dir = iv_remote_dir ).

    DATA(lv_json_payload) = /ui2/cl_json=>serialize(
                              data        = ls_request
                              pretty_name = /ui2/cl_json=>pretty_mode-low_case ).

* 提前声明接收变量
    DATA: lv_response TYPE string.

* 调用统一 HTTP 引擎
    DATA(lv_status) = _call_rest_api(
      EXPORTING iv_uri           = '/api/sftp/delete'
                iv_payload       = lv_json_payload
      IMPORTING ev_response_body = lv_response ).

    IF lv_status = 200.
      rv_ok = abap_true.
    ELSE.
      MESSAGE |删除失败[{ lv_status }]: { lv_response }| TYPE 'E'.
    ENDIF.
  ENDMETHOD.


  METHOD _call_rest_api.
*----------------------------------------------------------------------*
* [底层引擎] 私有 HTTP 调用抽象
*----------------------------------------------------------------------*
    DATA: lo_client TYPE REF TO if_http_client.
    rv_status = 500.

* 使用传统的 CL_HTTP_CLIENT 调用 SM59
    cl_http_client=>create_by_destination(
      EXPORTING
        destination              = 'Z_PYTHON_SFTP_API'
      IMPORTING
        client                   = lo_client
      EXCEPTIONS
        OTHERS                   = 1 ).

    IF sy-subrc <> 0.
      MESSAGE '创建HTTP客户端失败,请检查 SM59 (Z_PYTHON_SFTP_API)' TYPE 'E'.
      RETURN.
    ENDIF.

* 构造请求参数
    lo_client->request->set_method( if_http_request=>co_request_method_post ).
    cl_http_utility=>set_request_uri( request = lo_client->request uri = iv_uri ).
    lo_client->request->set_content_type( 'application/json' ).
    lo_client->request->set_cdata( data = iv_payload ).

* 发送请求
    lo_client->send( EXCEPTIONS OTHERS = 1 ).
    IF sy-subrc <> 0.
      lo_client->get_last_error( IMPORTING message = DATA(lv_err_send) ).
      lo_client->close( ).
      MESSAGE |发送请求失败: { lv_err_send }| TYPE 'E'.
      RETURN.
    ENDIF.

* 接收响应
    lo_client->receive( EXCEPTIONS OTHERS = 1 ).
    IF sy-subrc <> 0.
      lo_client->get_last_error( IMPORTING message = DATA(lv_err_recv) ).
      lo_client->close( ).
      MESSAGE |接收响应失败: { lv_err_recv }| TYPE 'E'.
      RETURN.
    ENDIF.

* 返回状态码与响应体
    lo_client->response->get_status( IMPORTING code = rv_status ).
    ev_response_body = lo_client->response->get_cdata( ).

    lo_client->close( ).
  ENDMETHOD.

ENDCLASS.


python 文件代码:

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import paramiko
import base64
import io
import traceback

app = FastAPI(title="SAP SFTP Bridge API")

# ---------------------------------------------------------
# 数据模型定义
# ---------------------------------------------------------
class SftpUploadRequest(BaseModel):
    filename: str
    content_base64: str
    remote_dir: str = "/"

class SftpBasicRequest(BaseModel):
    filename: str
    remote_dir: str = "/"

# ---------------------------------------------------------
# 辅助函数:自动递归创建远端多级目录 (类似 mkdir -p)
# ---------------------------------------------------------
def ensure_remote_dir(sftp, remote_dir):
    dirs = remote_dir.replace('\\', '/').split('/')
    current_dir = ''
    for d in dirs:
        if not d: 
            continue
        current_dir += '/' + d
        try:
            sftp.stat(current_dir)
        except IOError:
            try:
                sftp.mkdir(current_dir)
            except Exception as e:
                raise Exception(f"无法创建远端目录 '{current_dir}',请检查该层级权限: {str(e)}")

# ---------------------------------------------------------
# 接口 1:上传文件 (Upload)
# ---------------------------------------------------------
@app.post("/api/sftp/upload")
async def upload_to_sftp(request: SftpUploadRequest):
    host, port, username, password = '172.16.*.***', 8000, 'name', '***********'
    try:
        file_bytes = base64.b64decode(request.content_base64)
        file_stream = io.BytesIO(file_bytes)

        transport = paramiko.Transport((host, port))
        transport.connect(username=username, password=password)
        sftp = paramiko.SFTPClient.from_transport(transport)

        target_dir = request.remote_dir if request.remote_dir.endswith('/') else request.remote_dir + '/'
        final_path = target_dir + request.filename
        
        # 自动创建不存在的文件夹
        if target_dir != '/':
            ensure_remote_dir(sftp, target_dir)
        
        # 执行上传
        sftp.putfo(file_stream, final_path)

        sftp.close()
        transport.close()
        return {"status": "SUCCESS", "message": f"文件 '{request.filename}' 已成功上传至 {host}:{port}{final_path}"}

    except Exception as e:
        print("\n====== SFTP [Upload] Error ======")
        traceback.print_exc()
        raise HTTPException(status_code=500, detail=str(e))

# ---------------------------------------------------------
# 接口 2:下载文件 (Download)
# ---------------------------------------------------------
@app.post("/api/sftp/download")
async def download_from_sftp(request: SftpBasicRequest):
    host, port, username, password = '172.16.*.***', 8000, 'name', '***********'
    try:
        transport = paramiko.Transport((host, port))
        transport.connect(username=username, password=password)
        sftp = paramiko.SFTPClient.from_transport(transport)

        target_dir = request.remote_dir if request.remote_dir.endswith('/') else request.remote_dir + '/'
        final_path = target_dir + request.filename

        # 在内存中读取文件内容
        file_stream = io.BytesIO()
        sftp.getfo(final_path, file_stream)
        
        # 将二进制转回 Base64
        file_bytes = file_stream.getvalue()
        base64_str = base64.b64encode(file_bytes).decode('utf-8')

        sftp.close()
        transport.close()

        return {
            "status": "SUCCESS", 
            "message": f"文件 '{request.filename}' 下载成功",
            "content_base64": base64_str
        }

    except IOError as e:
        print("\n====== SFTP [Download] Error ======")
        traceback.print_exc()
        raise HTTPException(status_code=404, detail=f"文件不存在或权限拒绝: {str(e)}")
    except Exception as e:
        print("\n====== SFTP [Download] Error ======")
        traceback.print_exc()
        raise HTTPException(status_code=500, detail=str(e))

# ---------------------------------------------------------
# 接口 3:删除文件 (Delete)
# ---------------------------------------------------------
@app.post("/api/sftp/delete")
async def delete_from_sftp(request: SftpBasicRequest):
    host, port, username, password = '172.16.*.***', 8000, 'name', '***********'
    try:
        transport = paramiko.Transport((host, port))
        transport.connect(username=username, password=password)
        sftp = paramiko.SFTPClient.from_transport(transport)

        target_dir = request.remote_dir if request.remote_dir.endswith('/') else request.remote_dir + '/'
        final_path = target_dir + request.filename

        # 执行删除
        sftp.remove(final_path)

        sftp.close()
        transport.close()
        return {"status": "SUCCESS", "message": f"文件 '{request.filename}' 已被成功删除"}

    except IOError as e:
        print("\n====== SFTP [Delete] Error ======")
        traceback.print_exc()
        raise HTTPException(status_code=404, detail=f"文件不存在或无权限删除: {str(e)}")
    except Exception as e:
        print("\n====== SFTP [Delete] Error ======")
        traceback.print_exc()
        raise HTTPException(status_code=500, detail=str(e))

# ---------------------------------------------------------
# 服务启动入口
# ---------------------------------------------------------
if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8001)

我是把python代码运行在自己电脑上,那么就需要用电脑的IP地址与SFTP服务器链接

  1. 使用局域网 IP(依赖企业专线/VPN)
    这是企业开发中最标准的方法。通常你的公司网络与 SAP PCE(私有云)之间已经拉了 VPN 隧道 或 专线。

在你运行 Python 的本地电脑上,打开命令行(CMD),输入 ipconfig。

找到你的局域网 IP(通常是 10.x.x.x、172.x.x.x 或 192.168.x.x 格式)。

在 SM59 中配置:

目标主机:填入这个真实的局域网 IP(例如 172.16.5.43)。

服务编号/端口:8001。

注意网络防火墙:你本地电脑的防火墙(Windows Defender)默认可能会拦截入站流量。你必须在控制面板中为 Python 或 8001 端口添加一条“入站规则”,允许外部访问。

如果失败可以先用WinSCP测试一下自己电脑有没有连上。

后续可以把python文件布置在服务器上。

SAP 测试程序,可根据自己的需求调整,这只是一个简单的例子:

REPORT ztest_sftp_via_rest.

*----------------------------------------------------------------------*
* 屏幕定义 (Selection Screen Definition)
*----------------------------------------------------------------------*
SELECTION-SCREEN BEGIN OF BLOCK b_op WITH FRAME TITLE TEXT-001.
  " 操作模式选择
  PARAMETERS: p_upl RADIOBUTTON GROUP g1 DEFAULT 'X' USER-COMMAND uc1, " 上传
              p_dwn RADIOBUTTON GROUP g1,                              " 下载
              p_del RADIOBUTTON GROUP g1.                              " 删除
SELECTION-SCREEN END OF BLOCK b_op.

SELECTION-SCREEN BEGIN OF BLOCK b_path WITH FRAME TITLE TEXT-002.
  " 本地文件路径 (带 F4 搜索帮助)
  PARAMETERS: p_lfile  TYPE string LOWER CASE.
  " 远端文件名 (下载和删除时必须填写,上传时将自动从本地路径截取)
  PARAMETERS: p_rfile  TYPE string LOWER CASE.
  " 远端目标目录
  PARAMETERS: p_rmtdir TYPE string LOWER CASE DEFAULT '/share/sap/'.
SELECTION-SCREEN END OF BLOCK b_path.

*----------------------------------------------------------------------*
* 屏幕事件:动态校验必输项
*----------------------------------------------------------------------*
AT SELECTION-SCREEN.
  " 仅在点击执行按钮时校验
  IF sy-ucomm = 'ONLI'.
    IF p_upl = abap_true AND p_lfile IS INITIAL.
      MESSAGE '上传模式下,请指定本地文件路径' TYPE 'E'.
    ENDIF.
    IF p_dwn = abap_true.
      IF p_lfile IS INITIAL.
        MESSAGE '下载模式下,请指定本地保存路径 (通过 F4 选择)' TYPE 'E'.
      ENDIF.
      IF p_rfile IS INITIAL.
        MESSAGE '下载模式下,请明确指定远端文件名' TYPE 'E'.
      ENDIF.
    ENDIF.
    IF p_del = abap_true AND p_rfile IS INITIAL.
      MESSAGE '删除模式下,请明确指定远端文件名' TYPE 'E'.
    ENDIF.
  ENDIF.

*----------------------------------------------------------------------*
* 屏幕事件:本地文件 F4 搜索帮助 (智能区分打开与保存)
*----------------------------------------------------------------------*
AT SELECTION-SCREEN ON VALUE-REQUEST FOR p_lfile.
  DATA: lt_files TYPE filetable,
        lv_rc    TYPE i,
        lv_path  TYPE string,
        lv_fname TYPE string.

  IF p_upl = abap_true.
    " 上传模式:弹出文件选择窗口
    cl_gui_frontend_services=>file_open_dialog(
      EXPORTING window_title            = '请选择要上传的文件'
      CHANGING  file_table              = lt_files
                rc                      = lv_rc
      EXCEPTIONS OTHERS                 = 5 ).
    IF sy-subrc = 0 AND lv_rc > 0.
      p_lfile = lt_files[ 1 ]-filename.
    ENDIF.

  ELSEIF p_dwn = abap_true.
    " 下载模式:弹出文件保存窗口
    cl_gui_frontend_services=>file_save_dialog(
      EXPORTING window_title            = '请选择下载后的保存位置'
                default_file_name       = p_rfile " 默认保存名与远端同名
      CHANGING  filename                = lv_fname
                path                    = lv_path
                fullpath                = p_lfile
      EXCEPTIONS OTHERS                 = 5 ).
  ELSE.
    MESSAGE '删除操作无需选择本地文件' TYPE 'S'.
  ENDIF.


*----------------------------------------------------------------------*
* 主程序逻辑 (Main Logic)
*----------------------------------------------------------------------*
START-OF-SELECTION.
  DATA: lt_data     TYPE STANDARD TABLE OF x255,
        lv_length   TYPE i,
        lv_xstr     TYPE xstring,
        lv_filename TYPE string.

  DATA(lo_sftp) = NEW zcl_sftp_via_rest( ).
  DATA(lv_ok)   = abap_false.

  " ====================================================================
  " 模式 1:上传逻辑 (Upload)
  " ====================================================================
  IF p_upl = abap_true.
    " 1. 读取本地文件
    cl_gui_frontend_services=>gui_upload(
      EXPORTING filename   = p_lfile
                filetype   = 'BIN'
      IMPORTING filelength = lv_length
      CHANGING  data_tab   = lt_data
      EXCEPTIONS OTHERS    = 19 ).
    IF sy-subrc <> 0.
      MESSAGE '读取本地文件失败' TYPE 'E'.
      RETURN.
    ENDIF.

    " 2. 转为 XSTRING
    CALL FUNCTION 'SCMS_BINARY_TO_XSTRING'
      EXPORTING input_length = lv_length
      IMPORTING buffer       = lv_xstr
      TABLES    binary_tab   = lt_data
      EXCEPTIONS OTHERS      = 2.

    " 3. 提取文件名
    CALL FUNCTION 'SO_SPLIT_FILE_AND_PATH'
      EXPORTING full_name     = p_lfile
      IMPORTING stripped_name = lv_filename
      EXCEPTIONS OTHERS       = 1.

    " 4. 执行上传
    lv_ok = lo_sftp->upload_file(
      iv_filename   = lv_filename
      iv_binary     = lv_xstr
      iv_remote_dir = p_rmtdir ).

    IF lv_ok = abap_true.
      MESSAGE |文件 [{ lv_filename }] 上传成功!| TYPE 'S'.
    ENDIF.

  " ====================================================================
  " 模式 2:下载逻辑 (Download)
  " ====================================================================
  ELSEIF p_dwn = abap_true.
    " 1. 执行下载,获取 XSTRING 二进制数据
    lv_ok = lo_sftp->download_file(
      EXPORTING iv_filename   = p_rfile
                iv_remote_dir = p_rmtdir
      IMPORTING ev_binary     = lv_xstr ).

    IF lv_ok = abap_true.
      " 2. 将 XSTRING 转换为内表
      CALL FUNCTION 'SCMS_XSTRING_TO_BINARY'
        EXPORTING buffer        = lv_xstr
        IMPORTING output_length = lv_length
        TABLES    binary_tab    = lt_data.

      " 3. 将文件写入本地电脑
      cl_gui_frontend_services=>gui_download(
        EXPORTING filename     = p_lfile
                  filetype     = 'BIN'
                  bin_filesize = lv_length
        CHANGING  data_tab     = lt_data
        EXCEPTIONS OTHERS      = 24 ).

      IF sy-subrc = 0.
        MESSAGE |文件 [{ p_rfile }] 下载并保存成功!| TYPE 'S'.
      ELSE.
        MESSAGE '写入本地电脑失败,请检查路径权限或文件是否被占用' TYPE 'E'.
      ENDIF.
    ENDIF.

  " ====================================================================
  " 模式 3:删除逻辑 (Delete)
  " ====================================================================
  ELSEIF p_del = abap_true.
    " 直接调用删除接口
    lv_ok = lo_sftp->delete_file(
      iv_filename   = p_rfile
      iv_remote_dir = p_rmtdir ).

    IF lv_ok = abap_true.
      MESSAGE |远端文件 [{ p_rfile }] 删除成功!| TYPE 'S'.
    ENDIF.

  ENDIF.

开发板推荐:天空星STM32F407VET6开发板

超高性价比 STM32主控 | 超高主频 | 一板兼容百芯 | 比赛神器 | 沉金彩色丝印

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值