另存为 – 永夜 https://www.shuijingwanwq.com 没有不值得去解决的问题,也没有不值得去学习的技术! Sat, 30 May 2026 10:32:12 +0000 zh-Hans hourly 1 https://wordpress.org/?v=7.0 解决 Linux 微信「另存为」对话框只显示两个文件夹的问题 https://www.shuijingwanwq.com/2026/05/28/13822/ https://www.shuijingwanwq.com/2026/05/28/13822/#respond Thu, 28 May 2026 02:50:46 +0000 https://www.shuijingwanwq.com/?p=13822 浏览量: 83

问题现象

在 Ubuntu 系统中使用原生 Linux 版微信(通常为 Flatpak 版本),当收到图片或文件并点击「另存为」时,弹出的文件保存对话框中只能看到两个目录:

  • 下载
  • xwechat_files

而本来期望看到的是整个用户目录下的所有文件夹,例如:文档、图片、视频、桌面等。如下图所示,对话框中几乎空荡荡,只有寥寥两项。

图中查找范围为 /home/wangqiang,名称列表里仅有「下载」和「xwechat_files」两个文件夹

原因分析

这是因为 Linux 版微信通常以 Flatpak 沙箱形式运行。为了提高安全性,Flatpak 默认限制应用程序只能访问极少数的系统目录(例如 ~/下载 以及应用自己的数据目录 ~/xwechat_files)。其他如 文档图片桌面 等目录,微信没有权限访问,因此在「另存为」对话框中自然看不到。

这并非微信的 bug,而是 Flatpak 的权限隔离机制。好处是微信无法随意读取你的所有个人文件,坏处就是保存文件时很不方便。

解决方案

有两种方式可以解决:图形化工具(推荐新手)或终端命令(推荐熟悉命令行的用户)。

方案一:使用 Flatseal 图形化界面

  1. 安装 Flatseal
    打开终端,执行:
   flatpak install flathub com.github.tchx84.Flatseal
  1. 启动 Flatseal
    在应用程序菜单中找到并打开 Flatseal
  2. 为微信授权
  • 在左侧应用列表中找到 WeChat(或 com.tencent.WeChat)并单击。
  • 在右侧 Filesystem 区域,打开 All user files 开关(允许微信访问整个用户目录)。
  • 或者更精细地,点击「Other files」旁边的加号,添加 /home/wangqiang/文档/home/wangqiang/图片 等具体目录。
  1. 重启微信
    关闭微信再重新打开,再次点击「另存为」,会发现所有常用目录都出现了。

方案二:使用命令行(一次性授权)

如果更习惯使用终端,可以直接执行以下命令,授予微信访问整个用户家目录的权限:

sudo flatpak override com.tencent.WeChat --filesystem=home

如果只想授权特定目录(例如文档和图片):

sudo flatpak override com.tencent.WeChat --filesystem=/home/wangqiang/文档
sudo flatpak override com.tencent.WeChat --filesystem=/home/wangqiang/图片

执行后同样需要重启微信

如图2,可以看见用户目录下的所有目录了

临时变通方法(不修改权限)

如果不想修改微信的权限,也可以这样做:

  1. 在微信聊天窗口中,右键点击文件 → 选择「下载文件」。
  2. 再次右键同一个文件 → 选择「在文件夹中显示」。
  3. 文件管理器会打开 ~/xwechat_files 目录,里面就是已经下载的文件,可以从这里手动复制到任何其他位置。

这个方法不需要任何配置,但每次都要多几步操作。

延伸发现:保存的文件可能没有扩展名

在解决上述问题后,可能会遇到另一个小麻烦:从微信保存的图片,文件名往往只是一串数字或「1」「2」这样的无后缀名称。例如保存了一张图片,文件名叫 1(而不是 1.jpg)。当尝试上传到某些网页(如 DeepSeek)时,即使文件内容本身就是 JPEG,也可能因为缺少扩展名而被文件选择器过滤掉,导致「看不到文件」。

解决办法:批量给无扩展名的图片添加 .jpg 后缀。使用以下命令(安全版,只处理无点号的文件):

cd /path/to/your/images
for f in *; do
    [ -f "$f" ] && [[ "$f" != *.* ]] && mv -- "$f" "$f.jpg"
done

这样就能让图片在上传时被正常识别。

总结

问题原因解决方案
微信另存为只显示两个文件夹Flatpak 沙箱权限限制使用 Flatseal 或命令行开放文件系统权限
保存的图片没有扩展名微信 Linux 版保存逻辑不完善批量重命名添加 .jpg

通过以上设置, Linux 微信就能像 Windows 版一样自由选择保存路径了。希望这篇经验对同样使用 Ubuntu + 微信的朋友有帮助。


本文基于 Ubuntu 22.04/24.04 + Flatpak 版微信(com.tencent.WeChat)实测。不同版本可能略有差异,但原理相同。

]]>
https://www.shuijingwanwq.com/2026/05/28/13822/feed/ 0
基于 Yii 2 的 HTTP 客户端扩展,下载文件且另存为具体的文件名(下载远程文件至服务器),内存占用从 400 MB 下降至 7 MB 的实现 https://www.shuijingwanwq.com/2020/11/26/4627/ https://www.shuijingwanwq.com/2020/11/26/4627/#respond Thu, 26 Nov 2020 08:49:39 +0000 content]]> https://www.shuijingwanwq.com/?p=4627 浏览量: 99

1、现在已经实现:复制来源的资源文件至渠道发布的资源目录,返回相对路径(同步)。代码如下


    /**
     * 复制来源的资源文件至渠道发布的资源目录,返回相对路径(同步)
     * @param string $source 来源
     * 格式如下:spider
     * @param array $assets 来源的资源文件的绝对URL
     * 格式如下:
     * [
     *     [
     *         'type' => 'image',
     *         'absolute_url' => 'http://localhost/spider/storage/spider/images/1.png',
     *     ],
     *     [
     *         'type' => 'video',
     *         'absolute_url' => 'http://127.0.0.1/channel-pub-api/storage/spider/videos/7月份北上广深等十大城市租金环比上涨 看东方 20180820 高清_高清.mp4',
     *     ],
     * ]
     *
     * @return array $channelPubApiAssetAbsolutePaths 渠道发布的资源文件的相对路径
     * 格式如下:
     * [
     *     [
     *         'type' => 'image',
     *         'relative_path' => '/2018/09/20/1537439889.2333.1441541478.png',
     *     ],
     *     [
     *         'type' => 'video',
     *         'relative_path' => '/2018/09/20/1537439889.2403.62871268.mp4',
     *     ],
     * ]
     *
     * @throws UnprocessableEntityHttpException 如果来源的资源文件不是以来源的 HOME URL + BASE URL 开头,将抛出 422 HTTP 异常
     * @throws NotFoundHttpException 如果来源的资源文件不存在,将抛出 404 HTTP 异常
     * @throws ServerErrorHttpException 如果创建目录失败,将抛出 500 HTTP 异常
     * @throws ServerErrorHttpException 如果复制来源的资源文件至渠道发布的资源目录失败,将抛出 500 HTTP 异常
     * @throws \yii\base\Exception if the directory could not be created (i.e. php error due to parallel changes)
     */
    public static function copyAssetsSync($source, $assets)
    {
        // file_put_contents('/mcloud/www/channel-pub-api/console/runtime/copy-assets-sync-source-' . $source . '-' . time() . '.txt', $source);
        // file_put_contents('C:/phpStudy/PHPTutorial/WWW/channel-pub-api/douyin/runtime/copy-assets-sync-assets-' . $source . '-' . time() . '.txt', print_r($assets, true));
        // 不是以来源的 HOME URL + BASE URL 开头的来源的资源文件
        $notAbsoluteUrlStartKeys = [];
        // 不存在的来源的资源文件
        $notExistsKeys = [];
        // 渠道发布的资源文件的相对路径
        $channelPubApiAssetAbsolutePaths = [];

        foreach ($assets as $key => $asset) {
            if ($index = stripos($asset['absolute_url'], '?')) {
                $absoluteUrl = substr($asset['absolute_url'], 0, $index);
                $asset['absolute_url'] = $absoluteUrl;
                $assets[$key]['absolute_url'] = $absoluteUrl;
            }
            // 检查来源的资源文件的绝对URL是否以来源的 HOME URL + BASE URL 开头
            if (!StringHelper::startsWith($asset['absolute_url'], Yii::$app->params['source']['asset'][$asset['type']]['hostInfo'] . Yii::$app->params['source']['asset'][$asset['type']]['baseUrl'])) {
                $notAbsoluteUrlStartKeys[] = $asset['absolute_url'];
            } else {
                // 获取来源的资源文件的绝对路径
                $sourceAssetAbsolutePath = Yii::$app->params['source']['asset'][$asset['type']]['basePath'] . str_replace(Yii::$app->params['source']['asset'][$asset['type']]['hostInfo'] . Yii::$app->params['source']['asset'][$asset['type']]['baseUrl'], '', $asset['absolute_url']);
                // 检查来源的资源文件是否存在
                if (!file_exists($sourceAssetAbsolutePath)) {
                    $notExistsKeys[] = $sourceAssetAbsolutePath;
                }
            }
        }

        if (!empty($notAbsoluteUrlStartKeys)) {
            $notAbsoluteUrlStartKeys = implode(",", $notAbsoluteUrlStartKeys);
            throw new UnprocessableEntityHttpException(Yii::t('error', Yii::t('error', Yii::t('error', '202003'), ['not_absolute_url_start_keys' => $notAbsoluteUrlStartKeys])), 202003);
        }

        if (!empty($notExistsKeys)) {
            $notExistsKeys = implode(",", $notExistsKeys);
            throw new NotFoundHttpException(Yii::t('error', Yii::t('error', Yii::t('error', '202004'), ['not_exists_keys' => $notExistsKeys])), 202004);
        }

        foreach ($assets as $key => $asset) {
            // 获取来源的资源文件的绝对路径
            $sourceAssetAbsolutePath = Yii::$app->params['source']['asset'][$asset['type']]['basePath'] . str_replace(Yii::$app->params['source']['asset'][$asset['type']]['hostInfo'] . Yii::$app->params['source']['asset'][$asset['type']]['baseUrl'], '', $asset['absolute_url']);

            // 获取来源的资源文件的路径信息
            $pathInfo = pathinfo($sourceAssetAbsolutePath);
            // 渠道发布的资源文件的相对路径
            $directory = date('Y/m/d');
            $channelPubApiAssetRelativePath = '/' . $directory . '/' . microtime(true) . '.' . mt_rand() . '.' . $pathInfo['extension'];
            // 渠道发布的资源文件的绝对路径
            $channelPubApiAssetAbsolutePath = Yii::$app->params['channelPubApi']['asset'][$asset['type']]['basePath'] . $channelPubApiAssetRelativePath;
            // 创建目录
            if (!FileHelper::createDirectory(Yii::$app->params['channelPubApi']['asset'][$asset['type']]['basePath'] . '/' . $directory)) {
                throw new ServerErrorHttpException(Yii::t('error', Yii::t('error', Yii::t('error', '202005'), ['directory' => Yii::$app->params['channelPubApi']['asset'][$asset['type']]['basePath'] . '/' . $directory])), 202005);
            }
            // 复制来源的资源文件至渠道发布的资源目录
            if (!copy($sourceAssetAbsolutePath, $channelPubApiAssetAbsolutePath)) {
                throw new ServerErrorHttpException(Yii::t('error', Yii::t('error', Yii::t('error', '202006'), ['source_asset_absolute_path' => $sourceAssetAbsolutePath])), 202006);
            }

            $channelPubApiAssetAbsolutePaths[$key] = [
                'type' => $asset['type'],
                'relative_path' => $channelPubApiAssetRelativePath,
            ];
        }

        return $channelPubApiAssetAbsolutePaths;
    }


2、现在需要有所调整,能够再实现:下载文件且另存为具体的文件名,而不是复制。

3、先验证第一步骤的实现。调用此方法,打印响应结果,代码如下


        $copyAssetsResult = AssetService::copyAssetsSync(
            'spider',
            [
                [
                    'type' => 'image',
                    'absolute_url' => 'http://127.0.0.1/channel-pub-api/storage/spider/images/1.png',
                ],
                [
                    'type' => 'video',
                    'absolute_url' => 'http://127.0.0.1/channel-pub-api/storage/spider/videos/02018684a82381d9c59bb085e18e1a5d.mp4',
                ],
            ]
        );

        print_r($copyAssetsResult);
        exit;


4、打印响应结果,如图1

打印响应结果

图1


Array
(
    [0] => Array
        (
            [type] => image
            [relative_path] => /2020/11/24/1606187993.2607.476899553.png
        )

    [1] => Array
        (
            [type] => video
            [relative_path] => /2020/11/24/1606187993.2711.210785146.mp4
        )

)



5、查看渠道发布的资源目录中,复制后的文件。其绝对路径分别为:E:\wwwroot\channel-pub-api\storage\channel-pub-api\images\2020\11\24\1606187993.2607.476899553.png、E:\wwwroot\channel-pub-api\storage\channel-pub-api\videos\2020\11\24\1606187993.2711.210785146.mp4。如图2

查看渠道发布的资源目录中,复制后的文件。其绝对路径分别为:E:\wwwroot\channel-pub-api\storage\channel-pub-api\images\2020\11\24\1606187993.2607.476899553.png、E:\wwwroot\channel-pub-api\storage\channel-pub-api\videos\2020\11\24\1606187993.2711.210785146.mp4。

图2

6、实现 HTTP 模型,/common/logics/http/asset_api/Download.php

<?php
/**
 * Created by PhpStorm.
 * User: Qiang Wang
 * Date: 2020/11/24
 * Time: 15:47
 */
 
namespace common\logics\http\asset_api;
 
use Yii;
use yii\base\InvalidConfigException;
use yii\httpclient\Client;
use yii\httpclient\Exception;
use yii\web\ServerErrorHttpException;
 
/**
 * 资源接口的下载
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */
class Download extends Model
{
    /**
     * HTTP请求,基于来源的资源文件的绝对URL下载文件
     *
     * @param string $url 来源的资源文件的绝对URL
     *
     * @return string
     * 格式如下:
     *
     * @throws ServerErrorHttpException 如果响应状态码不等于20x
     * @throws InvalidConfigException
     * @throws Exception
     */
    public function download($url)
    {
        $client = new Client([
            'transport' => 'yii\httpclient\CurlTransport' // 只有 cURL 支持此选项
        ]);
        $response = $client->createRequest()
            ->setMethod('GET')
            ->setUrl($url)
            ->send();
        // 检查响应状态码是否等于20x
        if ($response->isOk) {
            return $response->content;
        } else {
            throw new ServerErrorHttpException(Yii::t('error', Yii::t('error', Yii::t('error', '202218'), ['status_code' => $response->getStatusCode()])), 202218);
        }
    }
}

7、新的实现:下载来源的资源文件后写入至渠道发布的资源目录,返回相对路径(同步)。代码如下


            /* HTTP请求,基于来源的资源文件的绝对URL下载文件 */
            $httpAssetApiDownload = new HttpAssetApiDownload();

            foreach ($assets as $key => $asset) {
                $downloadAsset = $httpAssetApiDownload->download($asset['absolute_url']);

                // 渠道发布的资源文件的相对路径
                $directory = date('Y/m/d');

                $channelPubApiAssetRelativePath = '/' . $directory . '/' . microtime(true) . '.' . mt_rand() . '.' . $asset['extension'];
                // 渠道发布的资源文件的绝对路径
                $channelPubApiAssetAbsolutePath = Yii::$app->params['channelPubApi']['asset'][$asset['type']]['basePath'] . $channelPubApiAssetRelativePath;
                // 创建目录
                if (!FileHelper::createDirectory(Yii::$app->params['channelPubApi']['asset'][$asset['type']]['basePath'] . '/' . $directory)) {
                    throw new ServerErrorHttpException(Yii::t('error', Yii::t('error', Yii::t('error', '202005'), ['directory' => Yii::$app->params['channelPubApi']['asset'][$asset['type']]['basePath'] . '/' . $directory])), 202005);
                }
                // 下载来源的资源文件后写入至渠道发布的资源目录
                if (!file_put_contents($channelPubApiAssetAbsolutePath, $downloadAsset, LOCK_EX)) {
                    throw new ServerErrorHttpException(Yii::t('error', Yii::t('error', Yii::t('error', '202220'), ['source_asset_absolute_url' => $asset['source_asset_absolute_url']])), 202220);
                }

                $channelPubApiAssetAbsolutePaths[$key] = [
                    'type' => $asset['type'],
                    'relative_path' => $channelPubApiAssetRelativePath,
                ];
            }


8、运行代码,视频文件的大小为:396 MB,报错:Allowed memory size of 134217728 bytes exhausted (tried to allocate 62918656 bytes)。如图3

运行代码,视频文件的大小为:396 MB,报错:Allowed memory size of 134217728 bytes exhausted (tried to allocate 62918656 bytes)。

图3


{
    "name": "PHP Fatal Error",
    "message": "Allowed memory size of 134217728 bytes exhausted (tried to allocate 62918656 bytes)",
    "code": 1,
    "type": "yii\\base\\ErrorException",
    "file": "E:\\wwwroot\\channel-pub-api\\vendor\\yiisoft\\yii2-httpclient\\src\\CurlTransport.php",
    "line": 40,
    "stack-trace": [
        "#0 [internal function]: yii\\base\\ErrorHandler->handleFatalError()",
        "#1 {main}"
    ]
}


9、编辑 php.ini 文件,修改 memory_limit = 128M 为 memory_limit = 1024M。设置为 512M 时仍然报错。运行成功。查看日志,占用内存:406 MB。如图4

编辑 php.ini 文件,修改 memory_limit = 128M 为 memory_limit = 1024M。设置为 512M 时仍然报错。运行成功。查看日志,占用内存:406 MB。

图4

10、查看渠道发布的资源目录中,复制后的文件。其绝对路径分别为:E:\wwwroot\channel-pub-api\storage\channel-pub-api\images\2020\11\25\1606291709.4136.2030184970.png、E:\wwwroot\channel-pub-api\storage\channel-pub-api\videos\2020\11\25\1606291735.722.2031039128.mp4。符合预期。如图5

查看渠道发布的资源目录中,复制后的文件。其绝对路径分别为:E:\wwwroot\channel-pub-api\storage\channel-pub-api\images\2020\11\25\1606291709.4136.2030184970.png、E:\wwwroot\channel-pub-api\storage\channel-pub-api\videos\2020\11\25\1606291735.722.2031039128.mp4。符合预期。

图5

11、此方案的问题在于:内存占用随着下载文件的大小而变化。可能因为下载文件的大小超出 memory_limit 而失败。决定再想办法优化完善。

12、重构 HTTP 模型,/common/logics/http/asset_api/Download.php。使用方法:setOutputFile()。与 yii\httpclient\CurlTransport 一起使用以设置传输写入的文件。如图6

重构 HTTP 模型,/common/logics/http/asset_api/Download.php。使用方法:setOutputFile()。与 yii\httpclient\CurlTransport 一起使用以设置传输写入的文件。

图6

<?php
/**
 * Created by PhpStorm.
 * User: Qiang Wang
 * Date: 2020/11/24
 * Time: 15:47
 */
 
namespace common\logics\http\asset_api;
 
use Yii;
use yii\base\InvalidConfigException;
use yii\httpclient\Client;
use yii\httpclient\Exception;
use yii\web\ServerErrorHttpException;
 
/**
 * 资源接口的下载
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */
class Download extends Model
{
    /**
     * HTTP请求,基于来源的资源文件的绝对URL下载文件
     *
     * @param string $url 来源的资源文件的绝对URL
     * @param string $absolutePath 资源文件的绝对路径(下载至的目标路径)
     *
     * @return bool
     * 格式如下:true
     *
     * @throws ServerErrorHttpException 如果响应状态码不等于20x
     * @throws InvalidConfigException
     * @throws Exception
     */
    public function download($url, $absolutePath)
    {
        // 打开即将下载的本地文件,在该文件上打开一个流
        $handle = fopen($absolutePath, 'w');
        if (!$handle) {
            throw new ServerErrorHttpException(Yii::t('error', Yii::t('error', Yii::t('error', '202220'), ['absolute_path' => $absolutePath])), 202220);
        }
 
        $client = new Client([
            'transport' => 'yii\httpclient\CurlTransport' // 只有 cURL 支持此选项
        ]);
        $response = $client->createRequest()
            ->setMethod('GET')
            ->setUrl($url)
            ->setOutputFile($handle)
            ->send();
 
        // 关闭一个已打开的文件指针
        fclose($handle);
 
        // 检查响应状态码是否等于20x
        if ($response->isOk) {
            return $response->content;
        } else {
            throw new ServerErrorHttpException(Yii::t('error', Yii::t('error', Yii::t('error', '202218'), ['status_code' => $response->getStatusCode()])), 202218);
        }
    }
}
 

13、重构新的实现:下载来源的资源文件同步写入至渠道发布的资源目录,返回相对路径(同步)。代码如下


            /* HTTP请求,基于来源的资源文件的绝对URL下载文件 */
            $httpAssetApiDownload = new HttpAssetApiDownload();

            foreach ($assets as $key => $asset) {

                // 渠道发布的资源文件的相对路径
                $directory = date('Y/m/d');

                $channelPubApiAssetRelativePath = '/' . $directory . '/' . microtime(true) . '.' . mt_rand() . '.' . $asset['extension'];
                // 渠道发布的资源文件的绝对路径
                $channelPubApiAssetAbsolutePath = Yii::$app->params['channelPubApi']['asset'][$asset['type']]['basePath'] . $channelPubApiAssetRelativePath;
                // 创建目录
                if (!FileHelper::createDirectory(Yii::$app->params['channelPubApi']['asset'][$asset['type']]['basePath'] . '/' . $directory)) {
                    throw new ServerErrorHttpException(Yii::t('error', Yii::t('error', Yii::t('error', '202005'), ['directory' => Yii::$app->params['channelPubApi']['asset'][$asset['type']]['basePath'] . '/' . $directory])), 202005);
                }

                // HTTP请求,基于来源的资源文件的绝对URL下载文件
                $httpAssetApiDownload->download($asset['absolute_url'], $channelPubApiAssetAbsolutePath);

                $channelPubApiAssetAbsolutePaths[$key] = [
                    'type' => $asset['type'],
                    'relative_path' => $channelPubApiAssetRelativePath,
                ];
            }


14、运行成功。查看日志,占用内存:6.377 MB。下载文件的大小不再受到 memory_limit 的限制。如图7

运行成功。查看日志,占用内存:6.377 MB。下载文件的大小不再受到 memory_limit 的限制。

图7

15、为何采用 cURL ,原因在于:cURL 的性能优于 file_get_contents 和 fopen。但是可以肯定的是内存占用优于 file_get_contents。未经测试,此结论为 PHP 官方文档所述。链接:https://www.php.net/manual/zh/ref.curl.php 。如图8

为何采用 cURL ,原因在于:cURL 的性能优于 file_get_contents 和 fopen。但是可以肯定的是内存占用优于 file_get_contents。未经测试,此结论为 PHP 官方文档所述。链接:https://www.php.net/manual/zh/ref.curl.php 。

图8

]]>
https://www.shuijingwanwq.com/2020/11/26/4627/feed/ 0