大家好!今天咱们来聊聊 WordPress 里一个相当重要的家伙,WP_Filesystem
抽象类。这家伙就像个“文件系统通用遥控器”,能让你用一套代码,操控各种不同的文件系统。听起来是不是有点厉害?
咱们先来理清一下概念。 想象一下,你要在服务器上创建个文件夹,或者读取个文件。不同的服务器环境,操作方式可能千差万别:有的用最简单的本地直接访问,有的需要通过 FTP,还有的得用 SSH。 如果每种情况你都得写一套代码,那简直要崩溃!
WP_Filesystem
的妙处就在于此。它定义了一组通用的文件操作接口(比如 mkdir
、put_contents
、get_contents
),然后通过不同的子类去实现这些接口。 这样,你就可以用统一的方式来操作文件,而不用关心底层到底用的是哪种文件系统。
咱们先来看看 WP_Filesystem
抽象类的核心代码(简化版,只包含关键部分):
abstract class WP_Filesystem {
/**
* Whether to use FTP or not.
*
* @var bool
*/
public $use_ftp = false;
/**
* Connect to the Filesystem.
*
* @param array $args Connection arguments.
* @return bool True on success, false on failure.
*/
abstract public function connect( $args = array() );
/**
* Get the last error (if any)
* @return WP_Error|false WP_Error on failure, false if no errors.
*/
public function errors() {
return false; // Default
}
/**
* Returns a list of filesystem errors.
*
* @return array Array of errors.
*/
public function get_error_messages() {
return array(); // Default.
}
/**
* Reads entire file into a string
*
* @param string $file Name of the file to read.
* @return string|false The file contents, or false on failure.
*/
abstract public function get_contents( $file );
/**
* Write a string to a file
*
* @param string $file Name of file to write to.
* @param string $contents The string to write to the file.
* @param int $mode (optional) The file permissions as octal number, eg. 0777.
* @return bool False on failure.
*/
abstract public function put_contents( $file, $contents, $mode = false );
/**
* Deletes a file.
*
* @param string $file Path to the file to delete.
* @param bool $recursive If set to true, deletes files and directories recursively.
* @param string $type Type of resource. 'f' for file, 'd' for directory.
* @return bool True on success, false on failure.
*/
abstract public function delete( $file, $recursive = false, $type = false );
/**
* Creates a directory.
*
* @param string $path Path to create.
* @param int $chmod Optional permissions (in octal format, e.g. 0777).
* @param bool $chown Optional whether to Chown the directory.
* @param bool $chgrp Optional whether to Chgrp the directory.
* @return bool True on success, false on failure.
*/
abstract public function mkdir( $path, $chmod = false, $chown = false, $chgrp = false );
// ... 其他文件操作方法 ...
}
注意几个关键点:
abstract class WP_Filesystem
:abstract
关键字意味着WP_Filesystem
本身不能被直接实例化。它只是一个“蓝图”,定义了一组必须实现的方法。abstract public function connect( $args = array() )
: 这是一个抽象方法,负责建立与文件系统的连接。每个子类都需要根据自己的文件系统类型来实现这个方法。abstract public function get_contents( $file )
、put_contents( $file, $contents, $mode = false )
、delete( $file, $recursive = false, $type = false )
、mkdir( $path, $chmod = false, $chown = false, $chgrp = false )
: 这些也是抽象方法,分别负责读取文件内容、写入文件内容、删除文件和创建目录。 同样,每个子类都需要实现这些方法。public $use_ftp = false;
: 这是一个公共属性,指示是否使用 FTP。
现在,咱们来看看几个 WP_Filesystem
的子类,看看它们是如何实现这些抽象方法的:
1. WP_Filesystem_Direct
这是最简单的一种,直接操作本地文件系统。 适用于服务器配置允许 PHP 直接读写文件的情况。
class WP_Filesystem_Direct extends WP_Filesystem_Base {
/**
* Constructor.
*
* @param array $args Not used.
*/
public function __construct( $args = array() ) {
$this->method = 'direct';
$this->strings = array(
'connecting' => __( 'Connecting to the filesystem...' ),
'connected' => __( 'Connected to the filesystem.' ),
);
}
/**
* Connect to the Filesystem.
*
* @return bool True on success, false on failure.
*/
public function connect() {
return true; // Direct connection always succeeds.
}
/**
* Reads entire file into a string
*
* @param string $file Name of the file to read.
* @return string|false The file contents, or false on failure.
*/
public function get_contents( $file ) {
return @file_get_contents( $file );
}
/**
* Write a string to a file
*
* @param string $file Name of file to write to.
* @param string $contents The string to write to the file.
* @param int $mode (optional) The file permissions as octal number, eg. 0777.
* @return bool False on failure.
*/
public function put_contents( $file, $contents, $mode = false ) {
if ( ! is_writable( dirname( $file ) ) ) {
return false;
}
if ( false === @file_put_contents( $file, $contents ) ) {
return false;
}
if ( $mode ) {
@chmod( $file, $mode );
}
return true;
}
/**
* Deletes a file.
*
* @param string $file Path to the file to delete.
* @param bool $recursive If set to true, deletes files and directories recursively.
* @param string $type Type of resource. 'f' for file, 'd' for directory.
* @return bool True on success, false on failure.
*/
public function delete( $file, $recursive = false, $type = false ) {
if ( empty( $file ) ) {
return false;
}
if ( 'f' == $type || is_file( $file ) ) {
return @unlink( $file );
}
if ( 'd' == $type || is_dir( $file ) ) {
if ( ! $recursive ) {
return @rmdir( $file );
}
return $this->rmdir( $file, $recursive );
}
return false;
}
/**
* Creates a directory.
*
* @param string $path Path to create.
* @param int $chmod Optional permissions (in octal format, e.g. 0777).
* @param bool $chown Optional whether to Chown the directory.
* @param bool $chgrp Optional whether to Chgrp the directory.
* @return bool True on success, false on failure.
*/
public function mkdir( $path, $chmod = false, $chown = false, $chgrp = false ) {
$res = @mkdir( $path );
if ( $res ) {
if ( $chmod ) {
@chmod( $path, $chmod );
}
if ( $chown ) {
@chown( $path, $chown );
}
if ( $chgrp ) {
@chgrp( $path, $chgrp );
}
}
return $res;
}
// ... 其他文件操作方法 ...
}
可以看到,WP_Filesystem_Direct
的 connect()
方法直接返回 true
,因为不需要额外的连接步骤。 而 get_contents()
、put_contents()
、delete()
、mkdir()
等方法,则直接调用 PHP 内置的文件操作函数,比如 file_get_contents()
、file_put_contents()
、unlink()
、mkdir()
等。
2. WP_Filesystem_SSH2
这种方式通过 SSH2 协议连接到服务器。需要 ssh2
PHP 扩展的支持。
class WP_Filesystem_SSH2 extends WP_Filesystem_Base {
/**
* SSH2 resource.
*
* @var resource
*/
public $connection;
/**
* SSH2 SFTP resource.
*
* @var resource
*/
public $sftp;
/**
* Constructor.
*
* @param array $args SSH2 connection arguments.
*/
public function __construct( $args = array() ) {
$this->method = 'ssh2';
$this->strings = array(
'connecting' => __( 'Connecting to SSH2 server...' ),
'connected' => __( 'SSH2 connected.' ),
'auth_failed' => __( 'SSH2 authentication failed.' ),
'fs_unavailable' => __( 'Could not retrieve filesystem information.' ),
'unable_to_connect' => __( 'Unable to connect to: %s' ),
);
}
/**
* Connect to the Filesystem.
*
* @param array $args Connection arguments.
* @return bool True on success, false on failure.
*/
public function connect( $args = array() ) {
$defaults = array(
'hostname' => '',
'username' => '',
'password' => '',
'port' => 22,
'private_key' => '',
'key_string' => '',
);
$args = wp_parse_args( $args, $defaults );
extract( $args, EXTR_SKIP );
if ( empty( $hostname ) ) {
return false;
}
$this->connection = @ssh2_connect( $hostname, $port );
if ( ! $this->connection ) {
$this->errors->add( 'unable_to_connect', sprintf( $this->strings['unable_to_connect'], $hostname ) );
return false;
}
// Try public key authentication first
if ( ! empty( $private_key ) && file_exists( $private_key ) && function_exists('ssh2_auth_pubkey_file') ) {
if ( empty( $username ) ) {
$username = 'anonymous';
}
$signature = '';
if ( !empty($key_string) ) {
$signature = $key_string;
}
if ( ! @ssh2_auth_pubkey_file( $this->connection, $username, $private_key, $signature ) ) {
$this->errors->add( 'auth_failed', $this->strings['auth_failed'] );
return false;
}
} elseif ( ! empty( $username ) && ! empty( $password ) ) {
if ( ! @ssh2_auth_password( $this->connection, $username, $password ) ) {
$this->errors->add( 'auth_failed', $this->strings['auth_failed'] );
return false;
}
} else {
$this->errors->add( 'auth_failed', $this->strings['auth_failed'] );
return false;
}
$this->sftp = @ssh2_sftp( $this->connection );
if ( ! $this->sftp ) {
$this->errors->add( 'fs_unavailable', $this->strings['fs_unavailable'] );
return false;
}
return true;
}
/**
* Reads entire file into a string
*
* @param string $file Name of the file to read.
* @return string|false The file contents, or false on failure.
*/
public function get_contents( $file ) {
$temp = @fopen( 'ssh2.sftp://' . intval( $this->sftp ) . $file, 'r' );
if ( ! $temp ) {
return false;
}
$contents = '';
while ( ! feof( $temp ) ) {
$contents .= fread( $temp, 8192 );
}
fclose( $temp );
return $contents;
}
/**
* Write a string to a file
*
* @param string $file Name of file to write to.
* @param string $contents The string to write to the file.
* @param int $mode (optional) The file permissions as octal number, eg. 0777.
* @return bool False on failure.
*/
public function put_contents( $file, $contents, $mode = false ) {
$temp = @fopen( 'ssh2.sftp://' . intval( $this->sftp ) . $file, 'w' );
if ( ! $temp ) {
return false;
}
$written = @fwrite( $temp, $contents );
fclose( $temp );
if ( $mode ) {
$this->chmod( $file, $mode );
}
return ( $written === strlen( $contents ) );
}
/**
* Deletes a file.
*
* @param string $file Path to the file to delete.
* @param bool $recursive If set to true, deletes files and directories recursively.
* @param string $type Type of resource. 'f' for file, 'd' for directory.
* @return bool True on success, false on failure.
*/
public function delete( $file, $recursive = false, $type = false ) {
if ( empty( $file ) ) {
return false;
}
$file = str_replace( '\', '/', $file ); // for win32, occasional problems deleting files.
if ( 'f' == $type || is_file( $file ) ) {
return @ssh2_sftp_unlink( $this->sftp, $file );
}
if ( 'd' == $type || is_dir( $file ) ) {
if ( ! $recursive ) {
return @ssh2_sftp_rmdir( $this->sftp, $file );
}
return $this->rmdir( $file, $recursive );
}
return false;
}
/**
* Creates a directory.
*
* @param string $path Path to create.
* @param int $chmod Optional permissions (in octal format, e.g. 0777).
* @param bool $chown Optional whether to Chown the directory.
* @param bool $chgrp Optional whether to Chgrp the directory.
* @return bool True on success, false on failure.
*/
public function mkdir( $path, $chmod = false, $chown = false, $chgrp = false ) {
$created = @ssh2_sftp_mkdir( $this->sftp, $path, 0777, true );
if ( $created ) {
if ( $chmod ) {
$this->chmod( $path, $chmod );
}
if ( $chown ) {
$this->chown( $path, $chown );
}
if ( $chgrp ) {
$this->chgrp( $path, $chgrp );
}
return true;
}
return false;
}
// ... 其他文件操作方法 ...
}
connect()
方法使用ssh2_connect()
函数建立 SSH 连接,然后使用ssh2_auth_password()
或者ssh2_auth_pubkey_file()
函数进行身份验证。get_contents()
和put_contents()
方法使用ssh2.sftp://
协议来读取和写入文件。delete()
和mkdir()
方法使用ssh2_sftp_unlink()
和ssh2_sftp_mkdir()
函数来删除文件和创建目录。
3. WP_Filesystem_FTPSockets
这种方式通过 FTP 协议连接到服务器。
class WP_Filesystem_FTPSockets extends WP_Filesystem_Base {
/**
* FTP resource.
*
* @var resource
*/
public $ftp;
/**
* Whether using SSL.
*
* @var bool
*/
public $use_ssl = false;
/**
* Whether connected.
*
* @var bool
*/
public $connected = false;
/**
* Whether passive mode is enabled.
*
* @var bool
*/
public $passive = false;
/**
* Constructor.
*
* @param array $args FTP connection arguments.
*/
public function __construct( $args = array() ) {
$this->method = 'ftpsockets';
$this->strings = array(
'connecting' => __( 'Connecting to FTP server...' ),
'connected' => __( 'FTP connected.' ),
'wrong_login' => __( 'Incorrect username or password.' ),
'ftp_login_failed' => __( 'FTP login failed.' ),
'unable_to_connect' => __( 'Unable to connect to: %s' ),
);
}
/**
* Connect to the Filesystem.
*
* @param array $args Connection arguments.
* @return bool True on success, false on failure.
*/
public function connect( $args = array() ) {
$defaults = array(
'hostname' => '',
'username' => '',
'password' => '',
'port' => 21,
'ssl' => false,
'timeout' => 30,
);
$args = wp_parse_args( $args, $defaults );
extract( $args, EXTR_SKIP );
$this->use_ssl = $ssl;
if ( empty( $hostname ) ) {
return false;
}
if ( $this->use_ssl ) {
$this->ftp = @ftp_ssl_connect( $hostname, $port, $timeout );
} else {
$this->ftp = @ftp_connect( $hostname, $port, $timeout );
}
if ( ! $this->ftp ) {
$this->errors->add( 'unable_to_connect', sprintf( $this->strings['unable_to_connect'], $hostname ) );
return false;
}
if ( ! @ftp_login( $this->ftp, $username, $password ) ) {
$this->errors->add( 'ftp_login_failed', $this->strings['wrong_login'] );
return false;
}
$this->connected = true;
return true;
}
/**
* Reads entire file into a string
*
* @param string $file Name of the file to read.
* @return string|false The file contents, or false on failure.
*/
public function get_contents( $file ) {
$temp = tmpfile();
if ( ! $temp ) {
return false;
}
if ( ! @ftp_fget( $this->ftp, $temp, $file, FTP_BINARY, 0 ) ) {
fclose( $temp );
return false;
}
fseek( $temp, 0 ); // rewind
$contents = '';
while ( ! feof( $temp ) ) {
$contents .= fread( $temp, 8192 );
}
fclose( $temp );
return $contents;
}
/**
* Write a string to a file
*
* @param string $file Name of file to write to.
* @param string $contents The string to write to the file.
* @param int $mode (optional) The file permissions as octal number, eg. 0777.
* @return bool False on failure.
*/
public function put_contents( $file, $contents, $mode = false ) {
$temp = tmpfile();
if ( ! $temp ) {
return false;
}
fwrite( $temp, $contents );
fseek( $temp, 0 ); // rewind
$res = @ftp_fput( $this->ftp, $file, $temp, FTP_BINARY );
fclose( $temp );
if ( ! $res ) {
return false;
}
if ( $mode ) {
$this->chmod( $file, $mode );
}
return true;
}
/**
* Deletes a file.
*
* @param string $file Path to the file to delete.
* @param bool $recursive If set to true, deletes files and directories recursively.
* @param string $type Type of resource. 'f' for file, 'd' for directory.
* @return bool True on success, false on failure.
*/
public function delete( $file, $recursive = false, $type = false ) {
if ( empty( $file ) ) {
return false;
}
if ( 'f' == $type || is_file( $file ) ) {
return @ftp_delete( $this->ftp, $file );
}
if ( 'd' == $type || is_dir( $file ) ) {
if ( ! $recursive ) {
return @ftp_rmdir( $this->ftp, $file );
}
return $this->rmdir( $file, $recursive );
}
return false;
}
/**
* Creates a directory.
*
* @param string $path Path to create.
* @param int $chmod Optional permissions (in octal format, e.g. 0777).
* @param bool $chown Optional whether to Chown the directory.
* @param bool $chgrp Optional whether to Chgrp the directory.
* @return bool True on success, false on failure.
*/
public function mkdir( $path, $chmod = false, $chown = false, $chgrp = false ) {
$created = @ftp_mkdir( $this->ftp, $path );
if ( $created ) {
if ( $chmod ) {
$this->chmod( $path, $chmod );
}
if ( $chown ) {
$this->chown( $path, $chown );
}
if ( $chgrp ) {
$this->chgrp( $path, $chgrp );
}
return true;
}
return false;
}
// ... 其他文件操作方法 ...
}
connect()
方法使用ftp_connect()
或ftp_ssl_connect()
函数建立 FTP 连接,然后使用ftp_login()
函数进行身份验证。get_contents()
和put_contents()
方法使用ftp_fget()
和ftp_fput()
函数来读取和写入文件,并使用临时文件来存储数据。delete()
和mkdir()
方法使用ftp_delete()
和ftp_mkdir()
函数来删除文件和创建目录。
总结一下,WP_Filesystem
的各个子类,就像下面这张表一样:
子类 | 连接方式 | 依赖的 PHP 函数/扩展 | 适用场景 |
---|---|---|---|
WP_Filesystem_Direct |
直接访问 | file_get_contents , file_put_contents , unlink , mkdir 等 |
服务器配置允许 PHP 直接读写文件,是最简单、最快速的方式。 |
WP_Filesystem_SSH2 |
SSH2 | ssh2_connect , ssh2_auth_password , ssh2_sftp_* 等 (需要 ssh2 扩展) |
需要通过 SSH 连接到服务器进行文件操作。安全性较高,但需要服务器和 PHP 环境都支持 SSH2。 |
WP_Filesystem_FTPSockets |
FTP | ftp_connect , ftp_login , ftp_* 等 |
需要通过 FTP 连接到服务器进行文件操作。 是最常用的方式之一,但安全性相对较低。 |
那么,WordPress 是如何选择使用哪个子类的呢?
WordPress 使用 WP_Filesystem()
函数来初始化 WP_Filesystem
对象。 这个函数会根据服务器环境和配置,自动选择合适的子类。
/**
* Initialize the WordPress filesystem, using direct, SSH, or FTP.
*
* @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
*
* @param array $args Connection arguments.
* @param bool $allow_relaxed_file_ownership Optional. Whether to allow relaxed file ownership.
* Default false.
* @return bool True on success, false on failure.
*/
function WP_Filesystem( $args = '', $allow_relaxed_file_ownership = false ) {
global $wp_filesystem;
if ( isset( $wp_filesystem ) ) {
return true;
}
$defaults = array(
'context' => ABSPATH,
'purge_cache' => true,
'allow_relaxed_file_ownership' => $allow_relaxed_file_ownership,
);
$args = wp_parse_args( $args, $defaults );
/*
* If the user has defined `FS_METHOD` as direct, then force the direct
* filesystem.
*/
if ( defined( 'FS_METHOD' ) && 'direct' === FS_METHOD ) {
$filesystem_class = 'WP_Filesystem_Direct';
} elseif ( defined( 'FS_METHOD' ) && 'ssh2' === FS_METHOD && extension_loaded( 'ssh2' ) ) {
$filesystem_class = 'WP_Filesystem_SSH2';
} elseif ( defined( 'FS_METHOD' ) && 'ftpsockets' === FS_METHOD ) {
$filesystem_class = 'WP_Filesystem_FTPSockets';
} elseif ( defined( 'FS_METHOD' ) && 'ftp' === FS_METHOD ) {
$filesystem_class = 'WP_Filesystem_FTP'; //Deprecated, use ftpsockets instead
} else {
// By default, try direct.
$filesystem_class = 'WP_Filesystem_Direct';
// If that doesn't work, try ftp.
if ( ! @is_writable( ABSPATH . 'wp-config.php' ) ) { // Use ABSPATH, as using WP_CONTENT_DIR, etc will give false positives if the `ftp_base` is set wrong.
$filesystem_class = 'WP_Filesystem_FTPSockets';
}
// If we have the ssh2 ext, let's try it.
if ( extension_loaded( 'ssh2' ) ) {
$filesystem_class = 'WP_Filesystem_SSH2';
}
}
if ( $filesystem_class ) {
require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php';
require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-' . strtolower( str_replace( 'WP_Filesystem_', '', $filesystem_class ) ) . '.php';
$wp_filesystem = new $filesystem_class( $args );
}
if ( ! $wp_filesystem ) {
return false;
}
if ( ! $wp_filesystem->connect() ) {
return false;
}
return true;
}
这段代码的逻辑大致如下:
- 检查
FS_METHOD
常量: 如果定义了FS_METHOD
常量,并且值为direct
、ssh2
、ftpsockets
或ftp
,则使用对应的文件系统子类。 - 自动选择: 如果没有定义
FS_METHOD
常量,则尝试以下顺序:- 首先尝试
WP_Filesystem_Direct
。 - 如果
ABSPATH . 'wp-config.php'
不可写,则尝试WP_Filesystem_FTPSockets
。 - 如果
ssh2
扩展已加载,则尝试WP_Filesystem_SSH2
。
- 首先尝试
- 加载文件并实例化: 根据选择的文件系统子类,加载对应的文件,并实例化该类。
- 建立连接: 调用
connect()
方法建立与文件系统的连接。
举个例子:
假设你的服务器配置允许 PHP 直接读写文件,并且没有定义 FS_METHOD
常量。 那么,WordPress 会首先尝试使用 WP_Filesystem_Direct
。 如果成功连接,后续的文件操作就会直接调用 PHP 内置的函数。
但如果你的服务器配置不允许 PHP 直接读写文件,并且定义了 FS_METHOD
常量为 ftpsockets
,那么 WordPress 就会使用 WP_Filesystem_FTPSockets
,并通过 FTP 协议进行文件操作。
为什么要这么设计?
WP_Filesystem
抽象类和它的子类,提供了一种灵活、可扩展的文件系统操作方式。 这种设计有以下几个优点:
- 代码复用: 开发者可以使用统一的 API 来操作不同的文件系统,而无需编写针对每种文件系统的特定代码。
- 可扩展性: 如果需要支持新的文件系统,只需要创建一个新的
WP_Filesystem
子类,并实现相应的抽象方法即可。 - 灵活性: WordPress 可以根据服务器环境和配置,自动选择合适的文件系统操作方式。
- 安全性: 通过 SSH2 等方式,可以提供更安全的文件操作。
如何使用 WP_Filesystem
?
要使用 WP_Filesystem
,首先需要初始化它:
global $wp_filesystem;
if ( empty( $wp_filesystem ) ) {
require_once ABSPATH . '/wp-admin/includes/file.php';
WP_Filesystem();
}
if ( ! $wp_filesystem ) {
// 处理初始化失败的情况
echo 'Failed to initialize WP_Filesystem';
return;
}
这段代码首先检查 $wp_filesystem
全局变量是否已经存在。 如果不存在,则加载 wp-admin/includes/file.php
文件,并调用 WP_Filesystem()
函数来初始化 $wp_filesystem
对象。
初始化成功后,就可以使用 $wp_filesystem
对象来进行文件操作了:
global $wp_filesystem;
// 创建目录
$wp_filesystem->mkdir( WP_CONTENT_DIR . '/my-plugin' );
// 写入文件
$wp_filesystem->put_contents( WP_CONTENT_DIR . '/my-plugin/my-file.txt', 'Hello, world!' );
// 读取文件
$content = $wp_filesystem->get_contents( WP_CONTENT_DIR . '/my-plugin/my-file.txt' );
echo $content; // 输出:Hello, world!
// 删除文件
$wp_filesystem->delete( WP_CONTENT_DIR . '/my-plugin/my-file.txt' );
// 删除目录
$wp_filesystem->rmdir( WP_CONTENT_DIR . '/my-plugin' );
一些注意事项:
- 在使用
WP_Filesystem
之前,一定要确保已经正确初始化了$wp_filesystem
对象。 WP_Filesystem
的某些方法可能会返回WP_Error
对象,表示操作失败。 应该检查返回值,并进行相应的错误处理。- 不同的文件系统子类,可能对文件权限、所有者等方面有不同的要求。 应该根据实际情况进行配置。
- 为了提高安全性,尽量使用 SSH2 或 FTPS 等加密协议进行文件操作。
WP_Filesystem
并非万能的,某些高级的文件系统操作可能无法通过它来实现。
总而言之,WP_Filesystem
抽象类是 WordPress 中一个非常重要的组成部分。 它提供了一种通用的文件系统操作接口,使得开发者可以方便地进行各种文件操作,而无需关心底层的文件系统类型。 理解 WP_Filesystem
的工作原理,对于开发 WordPress 插件和主题至关重要。
希望今天的讲座对大家有所帮助! 咱们下次再见!